diff --git a/.gitignore b/.gitignore index bb65f13..9df63b3 100755 --- a/.gitignore +++ b/.gitignore @@ -98,10 +98,31 @@ temp/ # Local Netlify folder .netlify -# Claude specific +# AI specific .claude/ +.cursor/ +.roo/ +.taskmaster/ +.cline/ +.windsurf/ # Database files *.db *.sqlite -*.sqlite3 \ No newline at end of file +*.sqlite3 + +logs +dev-debug.log +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +# OS specific + +# Task files +tasks.json +tasks/ diff --git a/README.md b/README.md index ad33afe..fcb505c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ -A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/claude-code), Anthropic's official CLI for AI-assisted coding. You can use it locally or remotely to view your active projects and sessions in claude code and make changes to them the same way you would do it in claude code CLI. This gives you a proper interface that works everywhere. +A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/claude-code), and [Cursor CLI](https://docs.cursor.com/en/cli/overview). You can use it locally or remotely to view your active projects and sessions in Claude Code or Cursor and make changes to them from everywhere (mobile or desktop). This gives you a proper interface that works everywhere. Supports models including **Claude Sonnet 4**, **Opus 4.1**, and **GPT-5** ## Screenshots @@ -25,6 +25,14 @@ A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/cla Responsive mobile design with touch navigation + + +

CLI Selection

+CLI Selection +
+Select between Claude Code and Cursor CLI + + @@ -34,11 +42,12 @@ A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/cla ## Features - **Responsive Design** - Works seamlessly across desktop, tablet, and mobile so you can also use Claude Code from mobile -- **Interactive Chat Interface** - Built-in chat interface for seamless communication with Claude Code -- **Integrated Shell Terminal** - Direct access to Claude Code CLI through built-in shell functionality +- **Interactive Chat Interface** - Built-in chat interface for seamless communication with Claude Code or Cursor +- **Integrated Shell Terminal** - Direct access to Claude Code or Cursor CLI through built-in shell functionality - **File Explorer** - Interactive file tree with syntax highlighting and live editing - **Git Explorer** - View, stage and commit your changes. You can also switch branches - **Session Management** - Resume conversations, manage multiple sessions, and track history +- **Model Compatibility** - Works with Claude Sonnet 4, Opus 4.1, and GPT-5 ## Quick Start @@ -46,7 +55,8 @@ A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/cla ### Prerequisites - [Node.js](https://nodejs.org/) v20 or higher -- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and configured +- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and configured, and/or +- [Cursor CLI](https://docs.cursor.com/en/cli/overview) installed and configured ### Installation @@ -108,9 +118,10 @@ The UI automatically discovers Claude Code projects from `~/.claude/projects/` a - **Visual Project Browser** - All available projects with metadata and session counts - **Project Actions** - Rename, delete, and organize projects - **Smart Navigation** - Quick access to recent projects and sessions +- **MCP support** - Add your own MCP servers through the UI #### Chat Interface -- **Use responsive chat or Claude Code CLI** - You can either use the adapted chat interface or use the shell button to connect to Claude Code CLI. +- **Use responsive chat or Claude Code/Cursor CLI** - You can either use the adapted chat interface or use the shell button to connect to your selected CLI. - **Real-time Communication** - Stream responses from Claude with WebSocket connection - **Session Management** - Resume previous conversations or start fresh sessions - **Message History** - Complete conversation history with timestamps and metadata @@ -152,7 +163,7 @@ The UI automatically discovers Claude Code projects from `~/.claude/projects/` a ### Backend (Node.js + Express) - **Express Server** - RESTful API with static file serving - **WebSocket Server** - Communication for chats and project refresh -- **Claude CLI Integration** - Process spawning and management +- **CLI Integration (Claude Code / Cursor)** - Process spawning and management - **Session Management** - JSONL parsing and conversation persistence - **File System API** - Exposing file browser for projects diff --git a/package-lock.json b/package-lock.json index 12fc9c1..ce68869 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-code-ui", - "version": "1.5.0", + "version": "1.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-code-ui", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@codemirror/lang-css": "^6.3.1", @@ -39,6 +39,8 @@ "react-dropzone": "^14.2.3", "react-markdown": "^10.1.0", "react-router-dom": "^6.8.1", + "sqlite": "^5.1.1", + "sqlite3": "^5.1.7", "tailwind-merge": "^3.3.1", "ws": "^8.14.2", "xterm": "^5.3.0", @@ -1002,6 +1004,13 @@ "node": ">=18" } }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, "node_modules/@img/sharp-libvips-linux-ppc64": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", @@ -1404,6 +1413,58 @@ "node": ">= 8" } }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1725,6 +1786,16 @@ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1958,6 +2029,13 @@ "license": "MIT", "peer": true }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -1992,6 +2070,46 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -2044,6 +2162,28 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -2348,6 +2488,138 @@ "node": ">= 0.8" } }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cacache/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/cacache/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, "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", @@ -2520,6 +2792,16 @@ "url": "https://polar.sh/cva" } }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2665,6 +2947,16 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -2684,6 +2976,13 @@ "node": ">= 6" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT", + "optional": true + }, "node_modules/concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", @@ -2727,6 +3026,13 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -2892,6 +3198,13 @@ "node": ">=4.0.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3011,6 +3324,29 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/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", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -3020,6 +3356,23 @@ "once": "^1.4.0" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "optional": true + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3357,6 +3710,43 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC", + "optional": true + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3380,6 +3770,79 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/gauge/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3487,6 +3950,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "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==", + "license": "ISC", + "optional": true + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3509,6 +3979,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -3571,6 +4048,13 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause", + "optional": true + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -3587,6 +4071,45 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -3619,6 +4142,45 @@ ], "license": "BSD-3-Clause" }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -3637,6 +4199,20 @@ "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", "license": "MIT" }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "optional": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -3754,6 +4330,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3817,6 +4400,13 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT", + "optional": true + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -4018,6 +4608,67 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/make-fetch-happen/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4749,6 +5400,207 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-fetch/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -4888,6 +5740,31 @@ } } }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -4899,6 +5776,65 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-pty": { "version": "1.1.0-beta9", "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta9.tgz", @@ -4922,6 +5858,22 @@ "dev": true, "license": "MIT" }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4941,6 +5893,23 @@ "node": ">=0.10.0" } }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4992,6 +5961,22 @@ "wrappy": "1" } }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -5032,6 +6017,16 @@ "node": ">= 0.8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -5293,6 +6288,27 @@ "node": ">=10" } }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5627,6 +6643,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -5637,6 +6663,69 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/rollup": { "version": "4.45.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", @@ -5818,6 +6907,13 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -6273,6 +7369,47 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.6.tgz", + "integrity": "sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6298,6 +7435,82 @@ "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", "dev": true }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/sqlite": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/sqlite/-/sqlite-5.1.1.tgz", + "integrity": "sha512-oBkezXa2hnkfuJwUo44Hl9hS3er+YFtueifoajrgidvqsJRQFpc5fKoAkAor1O5ZnLoa28GBScfHXs8j0K358Q==", + "license": "MIT" + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/sqlite3/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ssri/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ssri/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -6625,6 +7838,23 @@ "node": ">=8.10.0" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tar-fs": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", @@ -6653,6 +7883,42 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -6859,6 +8125,26 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, "node_modules/unist-util-is": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", @@ -7159,6 +8445,61 @@ "node": ">= 8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wide-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", diff --git a/package.json b/package.json index 16d2f4f..ea084d1 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,8 @@ "react-dropzone": "^14.2.3", "react-markdown": "^10.1.0", "react-router-dom": "^6.8.1", + "sqlite": "^5.1.1", + "sqlite3": "^5.1.7", "tailwind-merge": "^3.3.1", "ws": "^8.14.2", "xterm": "^5.3.0", @@ -68,4 +70,4 @@ "tailwindcss": "^3.4.0", "vite": "^7.0.4" } -} \ No newline at end of file +} diff --git a/public/icons/cursor.svg b/public/icons/cursor.svg new file mode 100644 index 0000000..abadee5 --- /dev/null +++ b/public/icons/cursor.svg @@ -0,0 +1 @@ +Cursor \ No newline at end of file diff --git a/public/screenshots/cli-selection.png b/public/screenshots/cli-selection.png new file mode 100644 index 0000000..507cfce Binary files /dev/null and b/public/screenshots/cli-selection.png differ diff --git a/server/cursor-cli.js b/server/cursor-cli.js new file mode 100644 index 0000000..be471f9 --- /dev/null +++ b/server/cursor-cli.js @@ -0,0 +1,250 @@ +import { spawn } from 'child_process'; +import crossSpawn from 'cross-spawn'; +import { promises as fs } from 'fs'; +import path from 'path'; +import os from 'os'; + +// Use cross-spawn on Windows for better command execution +const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn; + +let activeCursorProcesses = new Map(); // Track active processes by session ID + +async function spawnCursor(command, options = {}, ws) { + return new Promise(async (resolve, reject) => { + const { sessionId, projectPath, cwd, resume, toolsSettings, skipPermissions, model, images } = options; + let capturedSessionId = sessionId; // Track session ID throughout the process + let sessionCreatedSent = false; // Track if we've already sent session-created event + let messageBuffer = ''; // Buffer for accumulating assistant messages + + // Use tools settings passed from frontend, or defaults + const settings = toolsSettings || { + allowedShellCommands: [], + skipPermissions: false + }; + + // Build Cursor CLI command + const args = []; + + // Build flags allowing both resume and prompt together (reply in existing session) + // Treat presence of sessionId as intention to resume, regardless of resume flag + if (sessionId) { + args.push('--resume=' + sessionId); + } + + if (command && command.trim()) { + // Provide a prompt (works for both new and resumed sessions) + args.push('-p', command); + + // Add model flag if specified (only meaningful for new sessions; harmless on resume) + if (!sessionId && model) { + args.push('--model', model); + } + + // Request streaming JSON when we are providing a prompt + args.push('--output-format', 'stream-json'); + } + + // Add skip permissions flag if enabled + if (skipPermissions || settings.skipPermissions) { + args.push('-f'); + console.log('⚠️ Using -f flag (skip permissions)'); + } + + // Use cwd (actual project directory) instead of projectPath + const workingDir = cwd || projectPath || process.cwd(); + + console.log('Spawning Cursor CLI:', 'cursor-agent', args.join(' ')); + console.log('Working directory:', workingDir); + console.log('Session info - Input sessionId:', sessionId, 'Resume:', resume); + + const cursorProcess = spawnFunction('cursor-agent', args, { + cwd: workingDir, + stdio: ['pipe', 'pipe', 'pipe'], + env: { ...process.env } // Inherit all environment variables + }); + + // Store process reference for potential abort + const processKey = capturedSessionId || Date.now().toString(); + activeCursorProcesses.set(processKey, cursorProcess); + + // Handle stdout (streaming JSON responses) + cursorProcess.stdout.on('data', (data) => { + const rawOutput = data.toString(); + console.log('📤 Cursor CLI stdout:', rawOutput); + + const lines = rawOutput.split('\n').filter(line => line.trim()); + + for (const line of lines) { + try { + const response = JSON.parse(line); + console.log('📄 Parsed JSON response:', response); + + // Handle different message types + switch (response.type) { + case 'system': + if (response.subtype === 'init') { + // Capture session ID + if (response.session_id && !capturedSessionId) { + capturedSessionId = response.session_id; + console.log('📝 Captured session ID:', capturedSessionId); + + // Update process key with captured session ID + if (processKey !== capturedSessionId) { + activeCursorProcesses.delete(processKey); + activeCursorProcesses.set(capturedSessionId, cursorProcess); + } + + // Send session-created event only once for new sessions + if (!sessionId && !sessionCreatedSent) { + sessionCreatedSent = true; + ws.send(JSON.stringify({ + type: 'session-created', + sessionId: capturedSessionId, + model: response.model, + cwd: response.cwd + })); + } + } + + // Send system info to frontend + ws.send(JSON.stringify({ + type: 'cursor-system', + data: response + })); + } + break; + + case 'user': + // Forward user message + ws.send(JSON.stringify({ + type: 'cursor-user', + data: response + })); + break; + + case 'assistant': + // Accumulate assistant message chunks + if (response.message && response.message.content && response.message.content.length > 0) { + const textContent = response.message.content[0].text; + messageBuffer += textContent; + + // Send as Claude-compatible format for frontend + ws.send(JSON.stringify({ + type: 'claude-response', + data: { + type: 'content_block_delta', + delta: { + type: 'text_delta', + text: textContent + } + } + })); + } + break; + + case 'result': + // Session complete + console.log('Cursor session result:', response); + + // Send final message if we have buffered content + if (messageBuffer) { + ws.send(JSON.stringify({ + type: 'claude-response', + data: { + type: 'content_block_stop' + } + })); + } + + // Send completion event + ws.send(JSON.stringify({ + type: 'cursor-result', + data: response, + success: response.subtype === 'success' + })); + break; + + default: + // Forward any other message types + ws.send(JSON.stringify({ + type: 'cursor-response', + data: response + })); + } + } catch (parseError) { + console.log('📄 Non-JSON response:', line); + // If not JSON, send as raw text + ws.send(JSON.stringify({ + type: 'cursor-output', + data: line + })); + } + } + }); + + // Handle stderr + cursorProcess.stderr.on('data', (data) => { + console.error('Cursor CLI stderr:', data.toString()); + ws.send(JSON.stringify({ + type: 'cursor-error', + error: data.toString() + })); + }); + + // Handle process completion + cursorProcess.on('close', async (code) => { + console.log(`Cursor CLI process exited with code ${code}`); + + // Clean up process reference + const finalSessionId = capturedSessionId || sessionId || processKey; + activeCursorProcesses.delete(finalSessionId); + + ws.send(JSON.stringify({ + type: 'claude-complete', + exitCode: code, + isNewSession: !sessionId && !!command // Flag to indicate this was a new session + })); + + if (code === 0) { + resolve(); + } else { + reject(new Error(`Cursor CLI exited with code ${code}`)); + } + }); + + // Handle process errors + cursorProcess.on('error', (error) => { + console.error('Cursor CLI process error:', error); + + // Clean up process reference on error + const finalSessionId = capturedSessionId || sessionId || processKey; + activeCursorProcesses.delete(finalSessionId); + + ws.send(JSON.stringify({ + type: 'cursor-error', + error: error.message + })); + + reject(error); + }); + + // Close stdin since Cursor doesn't need interactive input + cursorProcess.stdin.end(); + }); +} + +function abortCursorSession(sessionId) { + const process = activeCursorProcesses.get(sessionId); + if (process) { + console.log(`🛑 Aborting Cursor session: ${sessionId}`); + process.kill('SIGTERM'); + activeCursorProcesses.delete(sessionId); + return true; + } + return false; +} + +export { + spawnCursor, + abortCursorSession +}; \ No newline at end of file diff --git a/server/index.js b/server/index.js index b6103a1..ca45133 100755 --- a/server/index.js +++ b/server/index.js @@ -38,9 +38,11 @@ import mime from 'mime-types'; import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } from './projects.js'; import { spawnClaude, abortClaudeSession } from './claude-cli.js'; +import { spawnCursor, abortCursorSession } from './cursor-cli.js'; import gitRoutes from './routes/git.js'; import authRoutes from './routes/auth.js'; import mcpRoutes from './routes/mcp.js'; +import cursorRoutes from './routes/cursor.js'; import { initializeDatabase } from './database/db.js'; import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js'; @@ -175,6 +177,9 @@ app.use('/api/git', authenticateToken, gitRoutes); // MCP API Routes (protected) app.use('/api/mcp', authenticateToken, mcpRoutes); +// Cursor API Routes (protected) +app.use('/api/cursor', authenticateToken, cursorRoutes); + // Static files served after API routes app.use(express.static(path.join(__dirname, '../dist'))); @@ -214,8 +219,22 @@ app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, re app.get('/api/projects/:projectName/sessions/:sessionId/messages', authenticateToken, async (req, res) => { try { const { projectName, sessionId } = req.params; - const messages = await getSessionMessages(projectName, sessionId); - res.json({ messages }); + const { limit, offset } = req.query; + + // Parse limit and offset if provided + const parsedLimit = limit ? parseInt(limit, 10) : null; + const parsedOffset = offset ? parseInt(offset, 10) : 0; + + const result = await getSessionMessages(projectName, sessionId, parsedLimit, parsedOffset); + + // Handle both old and new response formats + if (Array.isArray(result)) { + // Backward compatibility: no pagination parameters were provided + res.json({ messages: result }); + } else { + // New format with pagination info + res.json(result); + } } catch (error) { res.status(500).json({ error: error.message }); } @@ -460,12 +479,39 @@ function handleChatConnection(ws) { console.log('📁 Project:', data.options?.projectPath || 'Unknown'); console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New'); await spawnClaude(data.command, data.options, ws); + } else if (data.type === 'cursor-command') { + console.log('🖱️ Cursor message:', data.command || '[Continue/Resume]'); + console.log('📁 Project:', data.options?.cwd || 'Unknown'); + console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New'); + console.log('🤖 Model:', data.options?.model || 'default'); + await spawnCursor(data.command, data.options, ws); + } else if (data.type === 'cursor-resume') { + // Backward compatibility: treat as cursor-command with resume and no prompt + console.log('🖱️ Cursor resume session (compat):', data.sessionId); + await spawnCursor('', { + sessionId: data.sessionId, + resume: true, + cwd: data.options?.cwd + }, ws); } else if (data.type === 'abort-session') { console.log('🛑 Abort session request:', data.sessionId); - const success = abortClaudeSession(data.sessionId); + const provider = data.provider || 'claude'; + const success = provider === 'cursor' + ? abortCursorSession(data.sessionId) + : abortClaudeSession(data.sessionId); ws.send(JSON.stringify({ type: 'session-aborted', sessionId: data.sessionId, + provider, + success + })); + } else if (data.type === 'cursor-abort') { + console.log('🛑 Abort Cursor session:', data.sessionId); + const success = abortCursorSession(data.sessionId); + ws.send(JSON.stringify({ + type: 'session-aborted', + sessionId: data.sessionId, + provider: 'cursor', success })); } @@ -500,14 +546,17 @@ function handleShellConnection(ws) { const projectPath = data.projectPath || process.cwd(); const sessionId = data.sessionId; const hasSession = data.hasSession; + const provider = data.provider || 'claude'; console.log('🚀 Starting shell in:', projectPath); console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : 'New session'); + console.log('🤖 Provider:', provider); // First send a welcome message + const providerName = provider === 'cursor' ? 'Cursor' : 'Claude'; const welcomeMsg = hasSession ? - `\x1b[36mResuming Claude session ${sessionId} in: ${projectPath}\x1b[0m\r\n` : - `\x1b[36mStarting new Claude session in: ${projectPath}\x1b[0m\r\n`; + `\x1b[36mResuming ${providerName} session ${sessionId} in: ${projectPath}\x1b[0m\r\n` : + `\x1b[36mStarting new ${providerName} session in: ${projectPath}\x1b[0m\r\n`; ws.send(JSON.stringify({ type: 'output', @@ -515,20 +564,38 @@ function handleShellConnection(ws) { })); try { - // Prepare the shell command adapted to the platform + // Prepare the shell command adapted to the platform and provider let shellCommand; - if (os.platform() === 'win32') { - if (hasSession && sessionId) { - // Try to resume session, but with fallback to new session if it fails - shellCommand = `Set-Location -Path "${projectPath}"; claude --resume ${sessionId}; if ($LASTEXITCODE -ne 0) { claude }`; + if (provider === 'cursor') { + // Use cursor-agent command + if (os.platform() === 'win32') { + if (hasSession && sessionId) { + shellCommand = `Set-Location -Path "${projectPath}"; cursor-agent --resume="${sessionId}"`; + } else { + shellCommand = `Set-Location -Path "${projectPath}"; cursor-agent`; + } } else { - shellCommand = `Set-Location -Path "${projectPath}"; claude`; + if (hasSession && sessionId) { + shellCommand = `cd "${projectPath}" && cursor-agent --resume="${sessionId}"`; + } else { + shellCommand = `cd "${projectPath}" && cursor-agent`; + } } } else { - if (hasSession && sessionId) { - shellCommand = `cd "${projectPath}" && claude --resume ${sessionId} || claude`; + // Use claude command (default) + if (os.platform() === 'win32') { + if (hasSession && sessionId) { + // Try to resume session, but with fallback to new session if it fails + shellCommand = `Set-Location -Path "${projectPath}"; claude --resume ${sessionId}; if ($LASTEXITCODE -ne 0) { claude }`; + } else { + shellCommand = `Set-Location -Path "${projectPath}"; claude`; + } } else { - shellCommand = `cd "${projectPath}" && claude`; + if (hasSession && sessionId) { + shellCommand = `cd "${projectPath}" && claude --resume ${sessionId} || claude`; + } else { + shellCommand = `cd "${projectPath}" && claude`; + } } } @@ -1000,7 +1067,7 @@ async function startServer() { console.log(`Claude Code UI server running on http://0.0.0.0:${PORT}`); // Start watching the projects folder for changes - await setupProjectsWatcher(); // Re-enabled with better-sqlite3 + await setupProjectsWatcher(); }); } catch (error) { console.error('❌ Failed to start server:', error); diff --git a/server/projects.js b/server/projects.js index 23ee462..1f22895 100755 --- a/server/projects.js +++ b/server/projects.js @@ -2,6 +2,10 @@ import { promises as fs } from 'fs'; import fsSync from 'fs'; import path from 'path'; import readline from 'readline'; +import crypto from 'crypto'; +import sqlite3 from 'sqlite3'; +import { open } from 'sqlite'; +import os from 'os'; // Cache for extracted project directories const projectDirectoryCache = new Map(); @@ -207,6 +211,14 @@ async function getProjects() { console.warn(`Could not load sessions for project ${entry.name}:`, e.message); } + // Also fetch Cursor sessions for this project + try { + project.cursorSessions = await getCursorSessions(actualProjectDir); + } catch (e) { + console.warn(`Could not load Cursor sessions for project ${entry.name}:`, e.message); + project.cursorSessions = []; + } + projects.push(project); } } @@ -236,9 +248,17 @@ async function getProjects() { fullPath: actualProjectDir, isCustomName: !!projectConfig.displayName, isManuallyAdded: true, - sessions: [] + sessions: [], + cursorSessions: [] }; + // Try to fetch Cursor sessions for manual projects too + try { + project.cursorSessions = await getCursorSessions(actualProjectDir); + } catch (e) { + console.warn(`Could not load Cursor sessions for manual project ${projectName}:`, e.message); + } + projects.push(project); } } @@ -385,8 +405,8 @@ async function parseJsonlSessions(filePath) { ); } -// Get messages for a specific session -async function getSessionMessages(projectName, sessionId) { +// Get messages for a specific session with pagination support +async function getSessionMessages(projectName, sessionId, limit = null, offset = 0) { const projectDir = path.join(process.env.HOME, '.claude', 'projects', projectName); try { @@ -394,7 +414,7 @@ async function getSessionMessages(projectName, sessionId) { const jsonlFiles = files.filter(file => file.endsWith('.jsonl')); if (jsonlFiles.length === 0) { - return []; + return { messages: [], total: 0, hasMore: false }; } const messages = []; @@ -423,12 +443,34 @@ async function getSessionMessages(projectName, sessionId) { } // Sort messages by timestamp - return messages.sort((a, b) => + const sortedMessages = messages.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0) ); + + const total = sortedMessages.length; + + // If no limit is specified, return all messages (backward compatibility) + if (limit === null) { + return sortedMessages; + } + + // Apply pagination - for recent messages, we need to slice from the end + // offset 0 should give us the most recent messages + const startIndex = Math.max(0, total - offset - limit); + const endIndex = total - offset; + const paginatedMessages = sortedMessages.slice(startIndex, endIndex); + const hasMore = startIndex > 0; + + return { + messages: paginatedMessages, + total, + hasMore, + offset, + limit + }; } catch (error) { console.error(`Error reading messages for session ${sessionId}:`, error); - return []; + return limit === null ? [] : { messages: [], total: 0, hasMore: false }; } } @@ -593,6 +635,117 @@ async function addProjectManually(projectPath, displayName = null) { }; } +// Fetch Cursor sessions for a given project path +async function getCursorSessions(projectPath) { + try { + // Calculate cwdID hash for the project path (Cursor uses MD5 hash) + const cwdId = crypto.createHash('md5').update(projectPath).digest('hex'); + const cursorChatsPath = path.join(os.homedir(), '.cursor', 'chats', cwdId); + + // Check if the directory exists + try { + await fs.access(cursorChatsPath); + } catch (error) { + // No sessions for this project + return []; + } + + // List all session directories + const sessionDirs = await fs.readdir(cursorChatsPath); + const sessions = []; + + for (const sessionId of sessionDirs) { + const sessionPath = path.join(cursorChatsPath, sessionId); + const storeDbPath = path.join(sessionPath, 'store.db'); + + try { + // Check if store.db exists + await fs.access(storeDbPath); + + // Capture store.db mtime as a reliable fallback timestamp + let dbStatMtimeMs = null; + try { + const stat = await fs.stat(storeDbPath); + dbStatMtimeMs = stat.mtimeMs; + } catch (_) {} + + // Open SQLite database + const db = await open({ + filename: storeDbPath, + driver: sqlite3.Database, + mode: sqlite3.OPEN_READONLY + }); + + // Get metadata from meta table + const metaRows = await db.all(` + SELECT key, value FROM meta + `); + + // Parse metadata + let metadata = {}; + for (const row of metaRows) { + if (row.value) { + try { + // Try to decode as hex-encoded JSON + const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/); + if (hexMatch) { + const jsonStr = Buffer.from(row.value, 'hex').toString('utf8'); + metadata[row.key] = JSON.parse(jsonStr); + } else { + metadata[row.key] = row.value.toString(); + } + } catch (e) { + metadata[row.key] = row.value.toString(); + } + } + } + + // Get message count + const messageCountResult = await db.get(` + SELECT COUNT(*) as count FROM blobs + `); + + await db.close(); + + // Extract session info + const sessionName = metadata.title || metadata.sessionTitle || 'Untitled Session'; + + // Determine timestamp - prefer createdAt from metadata, fall back to db file mtime + let createdAt = null; + if (metadata.createdAt) { + createdAt = new Date(metadata.createdAt).toISOString(); + } else if (dbStatMtimeMs) { + createdAt = new Date(dbStatMtimeMs).toISOString(); + } else { + createdAt = new Date().toISOString(); + } + + sessions.push({ + id: sessionId, + name: sessionName, + createdAt: createdAt, + lastActivity: createdAt, // For compatibility with Claude sessions + messageCount: messageCountResult.count || 0, + projectPath: projectPath + }); + + } catch (error) { + console.warn(`Could not read Cursor session ${sessionId}:`, error.message); + } + } + + // Sort sessions by creation time (newest first) + sessions.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + + // Return only the first 5 sessions for performance + return sessions.slice(0, 5); + + } catch (error) { + console.error('Error fetching Cursor sessions:', error); + return []; + } +} + export { getProjects, diff --git a/server/routes/cursor.js b/server/routes/cursor.js new file mode 100644 index 0000000..5f7e873 --- /dev/null +++ b/server/routes/cursor.js @@ -0,0 +1,794 @@ +import express from 'express'; +import { promises as fs } from 'fs'; +import path from 'path'; +import os from 'os'; +import { spawn } from 'child_process'; +import sqlite3 from 'sqlite3'; +import { open } from 'sqlite'; +import crypto from 'crypto'; + +const router = express.Router(); + +// GET /api/cursor/config - Read Cursor CLI configuration +router.get('/config', async (req, res) => { + try { + const configPath = path.join(os.homedir(), '.cursor', 'cli-config.json'); + + try { + const configContent = await fs.readFile(configPath, 'utf8'); + const config = JSON.parse(configContent); + + res.json({ + success: true, + config: config, + path: configPath + }); + } catch (error) { + // Config doesn't exist or is invalid + console.log('Cursor config not found or invalid:', error.message); + + // Return default config + res.json({ + success: true, + config: { + version: 1, + model: { + modelId: "gpt-5", + displayName: "GPT-5" + }, + permissions: { + allow: [], + deny: [] + } + }, + isDefault: true + }); + } + } catch (error) { + console.error('Error reading Cursor config:', error); + res.status(500).json({ + error: 'Failed to read Cursor configuration', + details: error.message + }); + } +}); + +// POST /api/cursor/config - Update Cursor CLI configuration +router.post('/config', async (req, res) => { + try { + const { permissions, model } = req.body; + const configPath = path.join(os.homedir(), '.cursor', 'cli-config.json'); + + // Read existing config or create default + let config = { + version: 1, + editor: { + vimMode: false + }, + hasChangedDefaultModel: false, + privacyCache: { + ghostMode: false, + privacyMode: 3, + updatedAt: Date.now() + } + }; + + try { + const existing = await fs.readFile(configPath, 'utf8'); + config = JSON.parse(existing); + } catch (error) { + // Config doesn't exist, use defaults + console.log('Creating new Cursor config'); + } + + // Update permissions if provided + if (permissions) { + config.permissions = { + allow: permissions.allow || [], + deny: permissions.deny || [] + }; + } + + // Update model if provided + if (model) { + config.model = model; + config.hasChangedDefaultModel = true; + } + + // Ensure directory exists + const configDir = path.dirname(configPath); + await fs.mkdir(configDir, { recursive: true }); + + // Write updated config + await fs.writeFile(configPath, JSON.stringify(config, null, 2)); + + res.json({ + success: true, + config: config, + message: 'Cursor configuration updated successfully' + }); + } catch (error) { + console.error('Error updating Cursor config:', error); + res.status(500).json({ + error: 'Failed to update Cursor configuration', + details: error.message + }); + } +}); + +// GET /api/cursor/mcp - Read Cursor MCP servers configuration +router.get('/mcp', async (req, res) => { + try { + const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json'); + + try { + const mcpContent = await fs.readFile(mcpPath, 'utf8'); + const mcpConfig = JSON.parse(mcpContent); + + // Convert to UI-friendly format + const servers = []; + if (mcpConfig.mcpServers && typeof mcpConfig.mcpServers === 'object') { + for (const [name, config] of Object.entries(mcpConfig.mcpServers)) { + const server = { + id: name, + name: name, + type: 'stdio', + scope: 'cursor', + config: {}, + raw: config + }; + + // Determine transport type and extract config + if (config.command) { + server.type = 'stdio'; + server.config.command = config.command; + server.config.args = config.args || []; + server.config.env = config.env || {}; + } else if (config.url) { + server.type = config.transport || 'http'; + server.config.url = config.url; + server.config.headers = config.headers || {}; + } + + servers.push(server); + } + } + + res.json({ + success: true, + servers: servers, + path: mcpPath + }); + } catch (error) { + // MCP config doesn't exist + console.log('Cursor MCP config not found:', error.message); + res.json({ + success: true, + servers: [], + isDefault: true + }); + } + } catch (error) { + console.error('Error reading Cursor MCP config:', error); + res.status(500).json({ + error: 'Failed to read Cursor MCP configuration', + details: error.message + }); + } +}); + +// POST /api/cursor/mcp/add - Add MCP server to Cursor configuration +router.post('/mcp/add', async (req, res) => { + try { + const { name, type = 'stdio', command, args = [], url, headers = {}, env = {} } = req.body; + const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json'); + + console.log(`➕ Adding MCP server to Cursor config: ${name}`); + + // Read existing config or create new + let mcpConfig = { mcpServers: {} }; + + try { + const existing = await fs.readFile(mcpPath, 'utf8'); + mcpConfig = JSON.parse(existing); + if (!mcpConfig.mcpServers) { + mcpConfig.mcpServers = {}; + } + } catch (error) { + console.log('Creating new Cursor MCP config'); + } + + // Build server config based on type + let serverConfig = {}; + + if (type === 'stdio') { + serverConfig = { + command: command, + args: args, + env: env + }; + } else if (type === 'http' || type === 'sse') { + serverConfig = { + url: url, + transport: type, + headers: headers + }; + } + + // Add server to config + mcpConfig.mcpServers[name] = serverConfig; + + // Ensure directory exists + const mcpDir = path.dirname(mcpPath); + await fs.mkdir(mcpDir, { recursive: true }); + + // Write updated config + await fs.writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2)); + + res.json({ + success: true, + message: `MCP server "${name}" added to Cursor configuration`, + config: mcpConfig + }); + } catch (error) { + console.error('Error adding MCP server to Cursor:', error); + res.status(500).json({ + error: 'Failed to add MCP server', + details: error.message + }); + } +}); + +// DELETE /api/cursor/mcp/:name - Remove MCP server from Cursor configuration +router.delete('/mcp/:name', async (req, res) => { + try { + const { name } = req.params; + const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json'); + + console.log(`🗑️ Removing MCP server from Cursor config: ${name}`); + + // Read existing config + let mcpConfig = { mcpServers: {} }; + + try { + const existing = await fs.readFile(mcpPath, 'utf8'); + mcpConfig = JSON.parse(existing); + } catch (error) { + return res.status(404).json({ + error: 'Cursor MCP configuration not found' + }); + } + + // Check if server exists + if (!mcpConfig.mcpServers || !mcpConfig.mcpServers[name]) { + return res.status(404).json({ + error: `MCP server "${name}" not found in Cursor configuration` + }); + } + + // Remove server from config + delete mcpConfig.mcpServers[name]; + + // Write updated config + await fs.writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2)); + + res.json({ + success: true, + message: `MCP server "${name}" removed from Cursor configuration`, + config: mcpConfig + }); + } catch (error) { + console.error('Error removing MCP server from Cursor:', error); + res.status(500).json({ + error: 'Failed to remove MCP server', + details: error.message + }); + } +}); + +// POST /api/cursor/mcp/add-json - Add MCP server using JSON format +router.post('/mcp/add-json', async (req, res) => { + try { + const { name, jsonConfig } = req.body; + const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json'); + + console.log(`➕ Adding MCP server to Cursor config via JSON: ${name}`); + + // Validate and parse JSON config + let parsedConfig; + try { + parsedConfig = typeof jsonConfig === 'string' ? JSON.parse(jsonConfig) : jsonConfig; + } catch (parseError) { + return res.status(400).json({ + error: 'Invalid JSON configuration', + details: parseError.message + }); + } + + // Read existing config or create new + let mcpConfig = { mcpServers: {} }; + + try { + const existing = await fs.readFile(mcpPath, 'utf8'); + mcpConfig = JSON.parse(existing); + if (!mcpConfig.mcpServers) { + mcpConfig.mcpServers = {}; + } + } catch (error) { + console.log('Creating new Cursor MCP config'); + } + + // Add server to config + mcpConfig.mcpServers[name] = parsedConfig; + + // Ensure directory exists + const mcpDir = path.dirname(mcpPath); + await fs.mkdir(mcpDir, { recursive: true }); + + // Write updated config + await fs.writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2)); + + res.json({ + success: true, + message: `MCP server "${name}" added to Cursor configuration via JSON`, + config: mcpConfig + }); + } catch (error) { + console.error('Error adding MCP server to Cursor via JSON:', error); + res.status(500).json({ + error: 'Failed to add MCP server', + details: error.message + }); + } +}); + +// GET /api/cursor/sessions - Get Cursor sessions from SQLite database +router.get('/sessions', async (req, res) => { + try { + const { projectPath } = req.query; + + // Calculate cwdID hash for the project path (Cursor uses MD5 hash) + const cwdId = crypto.createHash('md5').update(projectPath || process.cwd()).digest('hex'); + const cursorChatsPath = path.join(os.homedir(), '.cursor', 'chats', cwdId); + + + // Check if the directory exists + try { + await fs.access(cursorChatsPath); + } catch (error) { + // No sessions for this project + return res.json({ + success: true, + sessions: [], + cwdId: cwdId, + path: cursorChatsPath + }); + } + + // List all session directories + const sessionDirs = await fs.readdir(cursorChatsPath); + const sessions = []; + + for (const sessionId of sessionDirs) { + const sessionPath = path.join(cursorChatsPath, sessionId); + const storeDbPath = path.join(sessionPath, 'store.db'); + let dbStatMtimeMs = null; + + try { + // Check if store.db exists + await fs.access(storeDbPath); + + // Capture store.db mtime as a reliable fallback timestamp (last activity) + try { + const stat = await fs.stat(storeDbPath); + dbStatMtimeMs = stat.mtimeMs; + } catch (_) {} + + // Open SQLite database + const db = await open({ + filename: storeDbPath, + driver: sqlite3.Database, + mode: sqlite3.OPEN_READONLY + }); + + // Get metadata from meta table + const metaRows = await db.all(` + SELECT key, value FROM meta + `); + + let sessionData = { + id: sessionId, + name: 'Untitled Session', + createdAt: null, + mode: null, + projectPath: projectPath, + lastMessage: null, + messageCount: 0 + }; + + // Parse meta table entries + for (const row of metaRows) { + if (row.value) { + try { + // Try to decode as hex-encoded JSON + const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/); + if (hexMatch) { + const jsonStr = Buffer.from(row.value, 'hex').toString('utf8'); + const data = JSON.parse(jsonStr); + + if (row.key === 'agent') { + sessionData.name = data.name || sessionData.name; + // Normalize createdAt to ISO string in milliseconds + let createdAt = data.createdAt; + if (typeof createdAt === 'number') { + if (createdAt < 1e12) { + createdAt = createdAt * 1000; // seconds -> ms + } + sessionData.createdAt = new Date(createdAt).toISOString(); + } else if (typeof createdAt === 'string') { + const n = Number(createdAt); + if (!Number.isNaN(n)) { + const ms = n < 1e12 ? n * 1000 : n; + sessionData.createdAt = new Date(ms).toISOString(); + } else { + // Assume it's already an ISO/date string + const d = new Date(createdAt); + sessionData.createdAt = isNaN(d.getTime()) ? null : d.toISOString(); + } + } else { + sessionData.createdAt = sessionData.createdAt || null; + } + sessionData.mode = data.mode; + sessionData.agentId = data.agentId; + sessionData.latestRootBlobId = data.latestRootBlobId; + } + } else { + // If not hex, use raw value for simple keys + if (row.key === 'name') { + sessionData.name = row.value.toString(); + } + } + } catch (e) { + console.log(`Could not parse meta value for key ${row.key}:`, e.message); + } + } + } + + // Get message count from JSON blobs only (actual messages, not DAG structure) + try { + const blobCount = await db.get(` + SELECT COUNT(*) as count + FROM blobs + WHERE substr(data, 1, 1) = X'7B' + `); + sessionData.messageCount = blobCount.count; + + // Get the most recent JSON blob for preview (actual message, not DAG structure) + const lastBlob = await db.get(` + SELECT data FROM blobs + WHERE substr(data, 1, 1) = X'7B' + ORDER BY rowid DESC + LIMIT 1 + `); + + if (lastBlob && lastBlob.data) { + try { + // Try to extract readable preview from blob (may contain binary with embedded JSON) + const raw = lastBlob.data.toString('utf8'); + let preview = ''; + // Attempt direct JSON parse + try { + const parsed = JSON.parse(raw); + if (parsed?.content) { + if (Array.isArray(parsed.content)) { + const firstText = parsed.content.find(p => p?.type === 'text' && p.text)?.text || ''; + preview = firstText; + } else if (typeof parsed.content === 'string') { + preview = parsed.content; + } + } + } catch (_) {} + if (!preview) { + // Strip non-printable and try to find JSON chunk + const cleaned = raw.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, ''); + const s = cleaned; + const start = s.indexOf('{'); + const end = s.lastIndexOf('}'); + if (start !== -1 && end > start) { + const jsonStr = s.slice(start, end + 1); + try { + const parsed = JSON.parse(jsonStr); + if (parsed?.content) { + if (Array.isArray(parsed.content)) { + const firstText = parsed.content.find(p => p?.type === 'text' && p.text)?.text || ''; + preview = firstText; + } else if (typeof parsed.content === 'string') { + preview = parsed.content; + } + } + } catch (_) { + preview = s; + } + } else { + preview = s; + } + } + if (preview && preview.length > 0) { + sessionData.lastMessage = preview.substring(0, 100) + (preview.length > 100 ? '...' : ''); + } + } catch (e) { + console.log('Could not parse blob data:', e.message); + } + } + } catch (e) { + console.log('Could not read blobs:', e.message); + } + + await db.close(); + + // Finalize createdAt: use parsed meta value when valid, else fall back to store.db mtime + if (!sessionData.createdAt) { + if (dbStatMtimeMs && Number.isFinite(dbStatMtimeMs)) { + sessionData.createdAt = new Date(dbStatMtimeMs).toISOString(); + } + } + + sessions.push(sessionData); + + } catch (error) { + console.log(`Could not read session ${sessionId}:`, error.message); + } + } + + // Fallback: ensure createdAt is a valid ISO string (use session directory mtime as last resort) + for (const s of sessions) { + if (!s.createdAt) { + try { + const sessionDir = path.join(cursorChatsPath, s.id); + const st = await fs.stat(sessionDir); + s.createdAt = new Date(st.mtimeMs).toISOString(); + } catch { + s.createdAt = new Date().toISOString(); + } + } + } + // Sort sessions by creation date (newest first) + sessions.sort((a, b) => { + if (!a.createdAt) return 1; + if (!b.createdAt) return -1; + return new Date(b.createdAt) - new Date(a.createdAt); + }); + + res.json({ + success: true, + sessions: sessions, + cwdId: cwdId, + path: cursorChatsPath + }); + + } catch (error) { + console.error('Error reading Cursor sessions:', error); + res.status(500).json({ + error: 'Failed to read Cursor sessions', + details: error.message + }); + } +}); + +// GET /api/cursor/sessions/:sessionId - Get specific Cursor session from SQLite +router.get('/sessions/:sessionId', async (req, res) => { + try { + const { sessionId } = req.params; + const { projectPath } = req.query; + + // Calculate cwdID hash for the project path + const cwdId = crypto.createHash('md5').update(projectPath || process.cwd()).digest('hex'); + const storeDbPath = path.join(os.homedir(), '.cursor', 'chats', cwdId, sessionId, 'store.db'); + + + // Open SQLite database + const db = await open({ + filename: storeDbPath, + driver: sqlite3.Database, + mode: sqlite3.OPEN_READONLY + }); + + // Get all blobs to build the DAG structure + const allBlobs = await db.all(` + SELECT rowid, id, data FROM blobs + `); + + // Build the DAG structure from parent-child relationships + const blobMap = new Map(); // id -> blob data + const parentRefs = new Map(); // blob id -> [parent blob ids] + const childRefs = new Map(); // blob id -> [child blob ids] + const jsonBlobs = []; // Clean JSON messages + + for (const blob of allBlobs) { + blobMap.set(blob.id, blob); + + // Check if this is a JSON blob (actual message) or protobuf (DAG structure) + if (blob.data && blob.data[0] === 0x7B) { // Starts with '{' - JSON blob + try { + const parsed = JSON.parse(blob.data.toString('utf8')); + jsonBlobs.push({ ...blob, parsed }); + } catch (e) { + console.log('Failed to parse JSON blob:', blob.rowid); + } + } else if (blob.data) { // Protobuf blob - extract parent references + const parents = []; + let i = 0; + + // Scan for parent references (0x0A 0x20 followed by 32-byte hash) + while (i < blob.data.length - 33) { + if (blob.data[i] === 0x0A && blob.data[i+1] === 0x20) { + const parentHash = blob.data.slice(i+2, i+34).toString('hex'); + if (blobMap.has(parentHash)) { + parents.push(parentHash); + } + i += 34; + } else { + i++; + } + } + + if (parents.length > 0) { + parentRefs.set(blob.id, parents); + // Update child references + for (const parentId of parents) { + if (!childRefs.has(parentId)) { + childRefs.set(parentId, []); + } + childRefs.get(parentId).push(blob.id); + } + } + } + } + + // Perform topological sort to get chronological order + const visited = new Set(); + const sorted = []; + + // DFS-based topological sort + function visit(nodeId) { + if (visited.has(nodeId)) return; + visited.add(nodeId); + + // Visit all parents first (dependencies) + const parents = parentRefs.get(nodeId) || []; + for (const parentId of parents) { + visit(parentId); + } + + // Add this node after all its parents + const blob = blobMap.get(nodeId); + if (blob) { + sorted.push(blob); + } + } + + // Start with nodes that have no parents (roots) + for (const blob of allBlobs) { + if (!parentRefs.has(blob.id)) { + visit(blob.id); + } + } + + // Visit any remaining nodes (disconnected components) + for (const blob of allBlobs) { + visit(blob.id); + } + + // Now extract JSON messages in the order they appear in the sorted DAG + const messageOrder = new Map(); // JSON blob id -> order index + let orderIndex = 0; + + for (const blob of sorted) { + // Check if this blob references any JSON messages + if (blob.data && blob.data[0] !== 0x7B) { // Protobuf blob + // Look for JSON blob references + for (const jsonBlob of jsonBlobs) { + try { + const jsonIdBytes = Buffer.from(jsonBlob.id, 'hex'); + if (blob.data.includes(jsonIdBytes)) { + if (!messageOrder.has(jsonBlob.id)) { + messageOrder.set(jsonBlob.id, orderIndex++); + } + } + } catch (e) { + // Skip if can't convert ID + } + } + } + } + + // Sort JSON blobs by their appearance order in the DAG + const sortedJsonBlobs = jsonBlobs.sort((a, b) => { + const orderA = messageOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER; + const orderB = messageOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER; + if (orderA !== orderB) return orderA - orderB; + // Fallback to rowid if not in order map + return a.rowid - b.rowid; + }); + + // Use sorted JSON blobs + const blobs = sortedJsonBlobs.map((blob, idx) => ({ + ...blob, + sequence_num: idx + 1, + original_rowid: blob.rowid + })); + + // Get metadata from meta table + const metaRows = await db.all(` + SELECT key, value FROM meta + `); + + // Parse metadata + let metadata = {}; + for (const row of metaRows) { + if (row.value) { + try { + // Try to decode as hex-encoded JSON + const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/); + if (hexMatch) { + const jsonStr = Buffer.from(row.value, 'hex').toString('utf8'); + metadata[row.key] = JSON.parse(jsonStr); + } else { + metadata[row.key] = row.value.toString(); + } + } catch (e) { + metadata[row.key] = row.value.toString(); + } + } + } + + // Extract messages from sorted JSON blobs + const messages = []; + for (const blob of blobs) { + try { + // We already parsed JSON blobs earlier + const parsed = blob.parsed; + + if (parsed) { + // Filter out ONLY system messages at the server level + // Check both direct role and nested message.role + const role = parsed?.role || parsed?.message?.role; + if (role === 'system') { + continue; // Skip only system messages + } + messages.push({ + id: blob.id, + sequence: blob.sequence_num, + rowid: blob.original_rowid, + content: parsed + }); + } + } catch (e) { + // Skip blobs that cause errors + console.log(`Skipping blob ${blob.id}: ${e.message}`); + } + } + + await db.close(); + + res.json({ + success: true, + session: { + id: sessionId, + projectPath: projectPath, + messages: messages, + metadata: metadata, + cwdId: cwdId + } + }); + + } catch (error) { + console.error('Error reading Cursor session:', error); + res.status(500).json({ + error: 'Failed to read Cursor session', + details: error.message + }); + } +}); + +export default router; \ No newline at end of file diff --git a/server/routes/git.js b/server/routes/git.js index b56b3e4..e6e9b96 100755 --- a/server/routes/git.js +++ b/server/routes/git.js @@ -56,7 +56,6 @@ router.get('/status', async (req, res) => { try { const projectPath = await getActualProjectPath(project); - console.log('Git status for project:', project, '-> path:', projectPath); // Validate git repository await validateGitRepository(projectPath); @@ -136,13 +135,16 @@ router.get('/diff', async (req, res) => { lines.map(line => `+${line}`).join('\n'); } else { // Get diff for tracked files - const { stdout } = await execAsync(`git diff HEAD -- "${file}"`, { cwd: projectPath }); - diff = stdout || ''; + // First check for unstaged changes (working tree vs index) + const { stdout: unstagedDiff } = await execAsync(`git diff -- "${file}"`, { cwd: projectPath }); - // If no unstaged changes, check for staged changes - if (!diff) { + if (unstagedDiff) { + // Show unstaged changes if they exist + diff = unstagedDiff; + } else { + // If no unstaged changes, check for staged changes (index vs HEAD) const { stdout: stagedDiff } = await execAsync(`git diff --cached -- "${file}"`, { cwd: projectPath }); - diff = stagedDiff; + diff = stagedDiff || ''; } } @@ -192,7 +194,6 @@ router.get('/branches', async (req, res) => { try { const projectPath = await getActualProjectPath(project); - console.log('Git branches for project:', project, '-> path:', projectPath); // Validate git repository await validateGitRepository(projectPath); diff --git a/src/App.jsx b/src/App.jsx index 14231f6..1cbd7eb 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -31,7 +31,7 @@ import { ThemeProvider } from './contexts/ThemeContext'; import { AuthProvider } from './contexts/AuthContext'; import ProtectedRoute from './components/ProtectedRoute'; import { useVersionCheck } from './hooks/useVersionCheck'; -import { api } from './utils/api'; +import { api, authenticatedFetch } from './utils/api'; // Main App component with routing @@ -192,6 +192,27 @@ function AppContent() { const response = await api.projects(); const data = await response.json(); + // Always fetch Cursor sessions for each project so we can combine views + for (let project of data) { + try { + const url = `/api/cursor/sessions?projectPath=${encodeURIComponent(project.fullPath || project.path)}`; + const cursorResponse = await authenticatedFetch(url); + if (cursorResponse.ok) { + const cursorData = await cursorResponse.json(); + if (cursorData.success && cursorData.sessions) { + project.cursorSessions = cursorData.sessions; + } else { + project.cursorSessions = []; + } + } else { + project.cursorSessions = []; + } + } catch (error) { + console.error(`Error fetching Cursor sessions for project ${project.name}:`, error); + project.cursorSessions = []; + } + } + // Optimize to preserve object references when data hasn't changed setProjects(prevProjects => { // If no previous projects, just set the new data @@ -210,7 +231,8 @@ function AppContent() { newProject.displayName !== prevProject.displayName || newProject.fullPath !== prevProject.fullPath || JSON.stringify(newProject.sessionMeta) !== JSON.stringify(prevProject.sessionMeta) || - JSON.stringify(newProject.sessions) !== JSON.stringify(prevProject.sessions) + JSON.stringify(newProject.sessions) !== JSON.stringify(prevProject.sessions) || + JSON.stringify(newProject.cursorSessions) !== JSON.stringify(prevProject.cursorSessions) ); }) || data.length !== prevProjects.length; @@ -236,16 +258,26 @@ function AppContent() { const shouldSwitchTab = !selectedSession || selectedSession.id !== sessionId; // Find the session across all projects for (const project of projects) { - const session = project.sessions?.find(s => s.id === sessionId); + let session = project.sessions?.find(s => s.id === sessionId); if (session) { setSelectedProject(project); - setSelectedSession(session); + setSelectedSession({ ...session, __provider: 'claude' }); // Only switch to chat tab if we're loading a different session if (shouldSwitchTab) { setActiveTab('chat'); } return; } + // Also check Cursor sessions + const cSession = project.cursorSessions?.find(s => s.id === sessionId); + if (cSession) { + setSelectedProject(project); + setSelectedSession({ ...cSession, __provider: 'cursor' }); + if (shouldSwitchTab) { + setActiveTab('chat'); + } + return; + } } // If session not found, it might be a newly created session @@ -270,6 +302,15 @@ function AppContent() { if (activeTab !== 'git' && activeTab !== 'preview') { setActiveTab('chat'); } + + // For Cursor sessions, we need to set the session ID differently + // since they're persistent and not created by Claude + const provider = localStorage.getItem('selected-provider') || 'claude'; + if (provider === 'cursor') { + // Cursor sessions have persistent IDs + sessionStorage.setItem('cursorSessionId', session.id); + } + if (isMobile) { setSidebarOpen(false); } diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index d61ddd3..e8cec93 100644 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -21,10 +21,12 @@ import ReactMarkdown from 'react-markdown'; import { useDropzone } from 'react-dropzone'; import TodoList from './TodoList'; import ClaudeLogo from './ClaudeLogo.jsx'; +import CursorLogo from './CursorLogo.jsx'; import ClaudeStatus from './ClaudeStatus'; import { MicButton } from './MicButton.jsx'; -import { api } from '../utils/api'; +import { api, authenticatedFetch } from '../utils/api'; + // Safe localStorage utility to handle quota exceeded errors const safeLocalStorage = { @@ -179,7 +181,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile )} ) : ( - /* Claude/Error messages on the left */ + /* Claude/Error/Tool messages on the left */
{!isGrouped && (
@@ -187,13 +189,21 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
!
+ ) : message.type === 'tool' ? ( +
+ 🔧 +
) : (
- + {(localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? ( + + ) : ( + + )}
)}
- {message.type === 'error' ? 'Error' : 'Claude'} + {message.type === 'error' ? 'Error' : message.type === 'tool' ? 'Tool' : ((localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? 'Cursor' : 'Claude')}
)} @@ -325,11 +335,9 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile })()} {message.toolInput && message.toolName !== 'Edit' && (() => { // Debug log to see what we're dealing with - console.log('Tool display - name:', message.toolName, 'input type:', typeof message.toolInput); // Special handling for Write tool if (message.toolName === 'Write') { - console.log('Write tool detected, toolInput:', message.toolInput); try { let input; // Handle both JSON string and already parsed object @@ -339,7 +347,6 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile input = message.toolInput; } - console.log('Parsed Write input:', input); if (input.file_path && input.content !== undefined) { return ( @@ -998,6 +1005,20 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
) : (
+ {/* Thinking accordion for reasoning */} + {message.reasoning && ( +
+ + 💭 Thinking... + +
+
+ {message.reasoning} +
+
+
+ )} + {message.type === 'assistant' ? (
{ + return localStorage.getItem('selected-provider') || 'claude'; + }); + const [cursorModel, setCursorModel] = useState(() => { + return localStorage.getItem('cursor-model') || 'gpt-5'; + }); + // When selecting a session from Sidebar, auto-switch provider to match session's origin + useEffect(() => { + if (selectedSession && selectedSession.__provider && selectedSession.__provider !== provider) { + setProvider(selectedSession.__provider); + localStorage.setItem('selected-provider', selectedSession.__provider); + } + }, [selectedSession]); + + // Load Cursor default model from config + useEffect(() => { + if (provider === 'cursor') { + fetch('/api/cursor/config', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('auth-token')}` + } + }) + .then(res => res.json()) + .then(data => { + if (data.success && data.config?.model?.modelId) { + // Map Cursor model IDs to our simplified names + const modelMap = { + 'gpt-5': 'gpt-5', + 'claude-4-sonnet': 'sonnet-4', + 'sonnet-4': 'sonnet-4', + 'claude-4-opus': 'opus-4.1', + 'opus-4.1': 'opus-4.1' + }; + const mappedModel = modelMap[data.config.model.modelId] || data.config.model.modelId; + if (!localStorage.getItem('cursor-model')) { + setCursorModel(mappedModel); + } + } + }) + .catch(err => console.error('Error loading Cursor config:', err)); + } + }, [provider]); // Memoized diff calculation to prevent recalculating on every render @@ -1164,21 +1235,356 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess }; }, []); - // Load session messages from API - const loadSessionMessages = useCallback(async (projectName, sessionId) => { + // Load session messages from API with pagination + const loadSessionMessages = useCallback(async (projectName, sessionId, loadMore = false) => { if (!projectName || !sessionId) return []; - setIsLoadingSessionMessages(true); + const isInitialLoad = !loadMore; + if (isInitialLoad) { + setIsLoadingSessionMessages(true); + } else { + setIsLoadingMoreMessages(true); + } + try { - const response = await api.sessionMessages(projectName, sessionId); + const currentOffset = loadMore ? messagesOffset : 0; + const response = await api.sessionMessages(projectName, sessionId, MESSAGES_PER_PAGE, currentOffset); if (!response.ok) { throw new Error('Failed to load session messages'); } const data = await response.json(); - return data.messages || []; + + // Handle paginated response + if (data.hasMore !== undefined) { + setHasMoreMessages(data.hasMore); + setTotalMessages(data.total); + setMessagesOffset(currentOffset + (data.messages?.length || 0)); + return data.messages || []; + } else { + // Backward compatibility for non-paginated response + const messages = data.messages || []; + setHasMoreMessages(false); + setTotalMessages(messages.length); + return messages; + } } catch (error) { console.error('Error loading session messages:', error); return []; + } finally { + if (isInitialLoad) { + setIsLoadingSessionMessages(false); + } else { + setIsLoadingMoreMessages(false); + } + } + }, [messagesOffset]); + + // Load Cursor session messages from SQLite via backend + const loadCursorSessionMessages = useCallback(async (projectPath, sessionId) => { + if (!projectPath || !sessionId) return []; + setIsLoadingSessionMessages(true); + try { + const url = `/api/cursor/sessions/${encodeURIComponent(sessionId)}?projectPath=${encodeURIComponent(projectPath)}`; + const res = await authenticatedFetch(url); + if (!res.ok) return []; + const data = await res.json(); + const blobs = data?.session?.messages || []; + const converted = []; + const toolUseMap = {}; // Map to store tool uses by ID for linking results + + // First pass: process all messages maintaining order + for (let blobIdx = 0; blobIdx < blobs.length; blobIdx++) { + const blob = blobs[blobIdx]; + const content = blob.content; + let text = ''; + let role = 'assistant'; + let reasoningText = null; // Move to outer scope + try { + // Handle different Cursor message formats + if (content?.role && content?.content) { + // Direct format: {"role":"user","content":[{"type":"text","text":"..."}]} + // Skip system messages + if (content.role === 'system') { + continue; + } + + // Handle tool messages + if (content.role === 'tool') { + // Tool result format - find the matching tool use message and update it + if (Array.isArray(content.content)) { + for (const item of content.content) { + if (item?.type === 'tool-result') { + // Map ApplyPatch to Edit for consistency + let toolName = item.toolName || 'Unknown Tool'; + if (toolName === 'ApplyPatch') { + toolName = 'Edit'; + } + const toolCallId = item.toolCallId || content.id; + const result = item.result || ''; + + // Store the tool result to be linked later + if (toolUseMap[toolCallId]) { + toolUseMap[toolCallId].toolResult = { + content: result, + isError: false + }; + } else { + // No matching tool use found, create a standalone result message + converted.push({ + type: 'assistant', + content: '', + timestamp: new Date(Date.now() + blobIdx * 1000), + blobId: blob.id, + sequence: blob.sequence, + rowid: blob.rowid, + isToolUse: true, + toolName: toolName, + toolId: toolCallId, + toolInput: null, + toolResult: { + content: result, + isError: false + } + }); + } + } + } + } + continue; // Don't add tool messages as regular messages + } else { + // User or assistant messages + role = content.role === 'user' ? 'user' : 'assistant'; + + if (Array.isArray(content.content)) { + // Extract text, reasoning, and tool calls from content array + const textParts = []; + + for (const part of content.content) { + if (part?.type === 'text' && part?.text) { + textParts.push(part.text); + } else if (part?.type === 'reasoning' && part?.text) { + // Handle reasoning type - will be displayed in a collapsible section + reasoningText = part.text; + } else if (part?.type === 'tool-call') { + // First, add any text/reasoning we've collected so far as a message + if (textParts.length > 0 || reasoningText) { + converted.push({ + type: role, + content: textParts.join('\n'), + reasoning: reasoningText, + timestamp: new Date(Date.now() + blobIdx * 1000), + blobId: blob.id, + sequence: blob.sequence, + rowid: blob.rowid + }); + textParts.length = 0; + reasoningText = null; + } + + // Tool call in assistant message - format like Claude Code + // Map ApplyPatch to Edit for consistency with Claude Code + let toolName = part.toolName || 'Unknown Tool'; + if (toolName === 'ApplyPatch') { + toolName = 'Edit'; + } + const toolId = part.toolCallId || `tool_${blobIdx}`; + + // Create a tool use message with Claude Code format + // Map Cursor args format to Claude Code format + let toolInput = part.args; + + if (toolName === 'Edit' && part.args) { + // ApplyPatch uses 'patch' format, convert to Edit format + if (part.args.patch) { + // Parse the patch to extract old and new content + const patchLines = part.args.patch.split('\n'); + let oldLines = []; + let newLines = []; + let inPatch = false; + + for (const line of patchLines) { + if (line.startsWith('@@')) { + inPatch = true; + } else if (inPatch) { + if (line.startsWith('-')) { + oldLines.push(line.substring(1)); + } else if (line.startsWith('+')) { + newLines.push(line.substring(1)); + } else if (line.startsWith(' ')) { + // Context line - add to both + oldLines.push(line.substring(1)); + newLines.push(line.substring(1)); + } + } + } + + const filePath = part.args.file_path; + const absolutePath = filePath && !filePath.startsWith('/') + ? `${projectPath}/${filePath}` + : filePath; + toolInput = { + file_path: absolutePath, + old_string: oldLines.join('\n') || part.args.patch, + new_string: newLines.join('\n') || part.args.patch + }; + } else { + // Direct edit format + toolInput = part.args; + } + } else if (toolName === 'Read' && part.args) { + // Map 'path' to 'file_path' + // Convert relative path to absolute if needed + const filePath = part.args.path || part.args.file_path; + const absolutePath = filePath && !filePath.startsWith('/') + ? `${projectPath}/${filePath}` + : filePath; + toolInput = { + file_path: absolutePath + }; + } else if (toolName === 'Write' && part.args) { + // Map fields for Write tool + const filePath = part.args.path || part.args.file_path; + const absolutePath = filePath && !filePath.startsWith('/') + ? `${projectPath}/${filePath}` + : filePath; + toolInput = { + file_path: absolutePath, + content: part.args.contents || part.args.content + }; + } + + const toolMessage = { + type: 'assistant', + content: '', + timestamp: new Date(Date.now() + blobIdx * 1000), + blobId: blob.id, + sequence: blob.sequence, + rowid: blob.rowid, + isToolUse: true, + toolName: toolName, + toolId: toolId, + toolInput: toolInput ? JSON.stringify(toolInput) : null, + toolResult: null // Will be filled when we get the tool result + }; + converted.push(toolMessage); + toolUseMap[toolId] = toolMessage; // Store for linking results + } else if (part?.type === 'tool_use') { + // Old format support + if (textParts.length > 0 || reasoningText) { + converted.push({ + type: role, + content: textParts.join('\n'), + reasoning: reasoningText, + timestamp: new Date(Date.now() + blobIdx * 1000), + blobId: blob.id, + sequence: blob.sequence, + rowid: blob.rowid + }); + textParts.length = 0; + reasoningText = null; + } + + const toolName = part.name || 'Unknown Tool'; + const toolId = part.id || `tool_${blobIdx}`; + + const toolMessage = { + type: 'assistant', + content: '', + timestamp: new Date(Date.now() + blobIdx * 1000), + blobId: blob.id, + sequence: blob.sequence, + rowid: blob.rowid, + isToolUse: true, + toolName: toolName, + toolId: toolId, + toolInput: part.input ? JSON.stringify(part.input) : null, + toolResult: null + }; + converted.push(toolMessage); + toolUseMap[toolId] = toolMessage; + } else if (typeof part === 'string') { + textParts.push(part); + } + } + + // Add any remaining text/reasoning + if (textParts.length > 0) { + text = textParts.join('\n'); + if (reasoningText && !text) { + // Just reasoning, no text + converted.push({ + type: role, + content: '', + reasoning: reasoningText, + timestamp: new Date(Date.now() + blobIdx * 1000), + blobId: blob.id, + sequence: blob.sequence, + rowid: blob.rowid + }); + text = ''; // Clear to avoid duplicate + } + } else { + text = ''; + } + } else if (typeof content.content === 'string') { + text = content.content; + } + } + } else if (content?.message?.role && content?.message?.content) { + // Nested message format + if (content.message.role === 'system') { + continue; + } + role = content.message.role === 'user' ? 'user' : 'assistant'; + if (Array.isArray(content.message.content)) { + text = content.message.content + .map(p => (typeof p === 'string' ? p : (p?.text || ''))) + .filter(Boolean) + .join('\n'); + } else if (typeof content.message.content === 'string') { + text = content.message.content; + } + } + } catch (e) { + console.log('Error parsing blob content:', e); + } + if (text && text.trim()) { + const message = { + type: role, + content: text, + timestamp: new Date(Date.now() + blobIdx * 1000), + blobId: blob.id, + sequence: blob.sequence, + rowid: blob.rowid + }; + + // Add reasoning if we have it + if (reasoningText) { + message.reasoning = reasoningText; + } + + converted.push(message); + } + } + + // Sort messages by sequence/rowid to maintain chronological order + converted.sort((a, b) => { + // First sort by sequence if available (clean 1,2,3... numbering) + if (a.sequence !== undefined && b.sequence !== undefined) { + return a.sequence - b.sequence; + } + // Then try rowid (original SQLite row IDs) + if (a.rowid !== undefined && b.rowid !== undefined) { + return a.rowid - b.rowid; + } + // Fallback to timestamp + return new Date(a.timestamp) - new Date(b.timestamp); + }); + + return converted; + } catch (e) { + console.error('Error loading Cursor session messages:', e); + return []; } finally { setIsLoadingSessionMessages(false); } @@ -1337,43 +1743,106 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess return scrollHeight - scrollTop - clientHeight < 50; }, []); - // Handle scroll events to detect when user manually scrolls up - const handleScroll = useCallback(() => { + // Handle scroll events to detect when user manually scrolls up and load more messages + const handleScroll = useCallback(async () => { if (scrollContainerRef.current) { + const container = scrollContainerRef.current; const nearBottom = isNearBottom(); setIsUserScrolledUp(!nearBottom); + + // Check if we should load more messages (scrolled near top) + const scrolledNearTop = container.scrollTop < 100; + const provider = localStorage.getItem('selected-provider') || 'claude'; + + if (scrolledNearTop && hasMoreMessages && !isLoadingMoreMessages && selectedSession && selectedProject && provider !== 'cursor') { + // Save current scroll position + const previousScrollHeight = container.scrollHeight; + const previousScrollTop = container.scrollTop; + + // Load more messages + const moreMessages = await loadSessionMessages(selectedProject.name, selectedSession.id, true); + + if (moreMessages.length > 0) { + // Prepend new messages to the existing ones + setSessionMessages(prev => [...moreMessages, ...prev]); + + // Restore scroll position after DOM update + setTimeout(() => { + if (scrollContainerRef.current) { + const newScrollHeight = scrollContainerRef.current.scrollHeight; + const scrollDiff = newScrollHeight - previousScrollHeight; + scrollContainerRef.current.scrollTop = previousScrollTop + scrollDiff; + } + }, 0); + } + } } - }, [isNearBottom]); + }, [isNearBottom, hasMoreMessages, isLoadingMoreMessages, selectedSession, selectedProject, loadSessionMessages]); useEffect(() => { // Load session messages when session changes const loadMessages = async () => { if (selectedSession && selectedProject) { - setCurrentSessionId(selectedSession.id); + const provider = localStorage.getItem('selected-provider') || 'claude'; - // Only load messages from API if this is a user-initiated session change - // For system-initiated changes, preserve existing messages and rely on WebSocket - if (!isSystemSessionChange) { - const messages = await loadSessionMessages(selectedProject.name, selectedSession.id); - setSessionMessages(messages); - // convertedMessages will be automatically updated via useMemo - // Scroll to bottom after loading session messages if auto-scroll is enabled - if (autoScrollToBottom) { - setTimeout(() => scrollToBottom(), 200); + // Reset pagination state when switching sessions + setMessagesOffset(0); + setHasMoreMessages(false); + setTotalMessages(0); + + if (provider === 'cursor') { + // For Cursor, set the session ID for resuming + setCurrentSessionId(selectedSession.id); + sessionStorage.setItem('cursorSessionId', selectedSession.id); + + // Only load messages from SQLite if this is NOT a system-initiated session change + // For system-initiated changes, preserve existing messages + if (!isSystemSessionChange) { + // Load historical messages for Cursor session from SQLite + const projectPath = selectedProject.fullPath || selectedProject.path; + const converted = await loadCursorSessionMessages(projectPath, selectedSession.id); + setSessionMessages([]); + setChatMessages(converted); + } else { + // Reset the flag after handling system session change + setIsSystemSessionChange(false); } } else { - // Reset the flag after handling system session change - setIsSystemSessionChange(false); + // For Claude, load messages normally with pagination + setCurrentSessionId(selectedSession.id); + + // Only load messages from API if this is a user-initiated session change + // For system-initiated changes, preserve existing messages and rely on WebSocket + if (!isSystemSessionChange) { + const messages = await loadSessionMessages(selectedProject.name, selectedSession.id, false); + setSessionMessages(messages); + // convertedMessages will be automatically updated via useMemo + // Scroll to bottom after loading session messages if auto-scroll is enabled + if (autoScrollToBottom) { + setTimeout(() => scrollToBottom(), 200); + } + } else { + // Reset the flag after handling system session change + setIsSystemSessionChange(false); + } } } else { - setChatMessages([]); - setSessionMessages([]); + // Only clear messages if this is NOT a system-initiated session change AND we're not loading + // During system session changes or while loading, preserve the chat messages + if (!isSystemSessionChange && !isLoading) { + setChatMessages([]); + setSessionMessages([]); + } setCurrentSessionId(null); + sessionStorage.removeItem('cursorSessionId'); + setMessagesOffset(0); + setHasMoreMessages(false); + setTotalMessages(0); } }; loadMessages(); - }, [selectedSession, selectedProject, loadSessionMessages, scrollToBottom, isSystemSessionChange]); + }, [selectedSession, selectedProject, loadCursorSessionMessages, scrollToBottom, isSystemSessionChange]); // Update chatMessages when convertedMessages changes useEffect(() => { @@ -1441,6 +1910,63 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess case 'claude-response': const messageData = latestMessage.data.message || latestMessage.data; + // Handle Cursor streaming format (content_block_delta / content_block_stop) + if (messageData && typeof messageData === 'object' && messageData.type) { + if (messageData.type === 'content_block_delta' && messageData.delta?.text) { + // Buffer deltas and flush periodically to reduce rerenders + streamBufferRef.current += messageData.delta.text; + if (!streamTimerRef.current) { + streamTimerRef.current = setTimeout(() => { + const chunk = streamBufferRef.current; + streamBufferRef.current = ''; + streamTimerRef.current = null; + if (!chunk) return; + setChatMessages(prev => { + const updated = [...prev]; + const last = updated[updated.length - 1]; + if (last && last.type === 'assistant' && !last.isToolUse && last.isStreaming) { + last.content = (last.content || '') + chunk; + } else { + updated.push({ type: 'assistant', content: chunk, timestamp: new Date(), isStreaming: true }); + } + return updated; + }); + }, 100); + } + return; + } + if (messageData.type === 'content_block_stop') { + // Flush any buffered text and mark streaming message complete + if (streamTimerRef.current) { + clearTimeout(streamTimerRef.current); + streamTimerRef.current = null; + } + const chunk = streamBufferRef.current; + streamBufferRef.current = ''; + if (chunk) { + setChatMessages(prev => { + const updated = [...prev]; + const last = updated[updated.length - 1]; + if (last && last.type === 'assistant' && !last.isToolUse && last.isStreaming) { + last.content = (last.content || '') + chunk; + } else { + updated.push({ type: 'assistant', content: chunk, timestamp: new Date(), isStreaming: true }); + } + return updated; + }); + } + setChatMessages(prev => { + const updated = [...prev]; + const last = updated[updated.length - 1]; + if (last && last.type === 'assistant' && last.isStreaming) { + last.isStreaming = false; + } + return updated; + }); + return; + } + } + // Handle Claude CLI session duplication bug workaround: // When resuming a session, Claude CLI creates a new session instead of resuming. // We detect this by checking for system/init messages with session_id that differs @@ -1581,11 +2107,30 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess break; case 'claude-output': - setChatMessages(prev => [...prev, { - type: 'assistant', - content: latestMessage.data, - timestamp: new Date() - }]); + { + const cleaned = String(latestMessage.data || ''); + if (cleaned.trim()) { + streamBufferRef.current += (streamBufferRef.current ? `\n${cleaned}` : cleaned); + if (!streamTimerRef.current) { + streamTimerRef.current = setTimeout(() => { + const chunk = streamBufferRef.current; + streamBufferRef.current = ''; + streamTimerRef.current = null; + if (!chunk) return; + setChatMessages(prev => { + const updated = [...prev]; + const last = updated[updated.length - 1]; + if (last && last.type === 'assistant' && !last.isToolUse && last.isStreaming) { + last.content = last.content ? `${last.content}\n${chunk}` : chunk; + } else { + updated.push({ type: 'assistant', content: chunk, timestamp: new Date(), isStreaming: true }); + } + return updated; + }); + }, 100); + } + } + } break; case 'claude-interactive-prompt': // Handle interactive prompts from CLI @@ -1605,6 +2150,145 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess }]); break; + case 'cursor-system': + // Handle Cursor system/init messages similar to Claude + try { + const cdata = latestMessage.data; + if (cdata && cdata.type === 'system' && cdata.subtype === 'init' && cdata.session_id) { + // If we already have a session and this differs, switch (duplication/redirect) + if (currentSessionId && cdata.session_id !== currentSessionId) { + console.log('🔄 Cursor session switch detected:', { originalSession: currentSessionId, newSession: cdata.session_id }); + setIsSystemSessionChange(true); + if (onNavigateToSession) { + onNavigateToSession(cdata.session_id); + } + return; + } + // If we don't yet have a session, adopt this one + if (!currentSessionId) { + console.log('🔄 Cursor new session init detected:', { newSession: cdata.session_id }); + setIsSystemSessionChange(true); + if (onNavigateToSession) { + onNavigateToSession(cdata.session_id); + } + return; + } + } + // For other cursor-system messages, avoid dumping raw objects to chat + } catch (e) { + console.warn('Error handling cursor-system message:', e); + } + break; + + case 'cursor-user': + // Handle Cursor user messages (usually echoes) + // Don't add user messages as they're already shown from input + break; + + case 'cursor-tool-use': + // Handle Cursor tool use messages + setChatMessages(prev => [...prev, { + type: 'assistant', + content: `Using tool: ${latestMessage.tool} ${latestMessage.input ? `with ${latestMessage.input}` : ''}`, + timestamp: new Date(), + isToolUse: true, + toolName: latestMessage.tool, + toolInput: latestMessage.input + }]); + break; + + case 'cursor-error': + // Show Cursor errors as error messages in chat + setChatMessages(prev => [...prev, { + type: 'error', + content: `Cursor error: ${latestMessage.error || 'Unknown error'}`, + timestamp: new Date() + }]); + break; + + case 'cursor-result': + // Handle Cursor completion and final result text + setIsLoading(false); + setCanAbortSession(false); + setClaudeStatus(null); + try { + const r = latestMessage.data || {}; + const textResult = typeof r.result === 'string' ? r.result : ''; + // Flush buffered deltas before finalizing + if (streamTimerRef.current) { + clearTimeout(streamTimerRef.current); + streamTimerRef.current = null; + } + const pendingChunk = streamBufferRef.current; + streamBufferRef.current = ''; + + setChatMessages(prev => { + const updated = [...prev]; + // Try to consolidate into the last streaming assistant message + const last = updated[updated.length - 1]; + if (last && last.type === 'assistant' && !last.isToolUse && last.isStreaming) { + // Replace streaming content with the final content so deltas don't remain + const finalContent = textResult && textResult.trim() ? textResult : (last.content || '') + (pendingChunk || ''); + last.content = finalContent; + last.isStreaming = false; + } else if (textResult && textResult.trim()) { + updated.push({ type: r.is_error ? 'error' : 'assistant', content: textResult, timestamp: new Date(), isStreaming: false }); + } + return updated; + }); + } catch (e) { + console.warn('Error handling cursor-result message:', e); + } + + // Mark session as inactive + const cursorSessionId = currentSessionId || sessionStorage.getItem('pendingSessionId'); + if (cursorSessionId && onSessionInactive) { + onSessionInactive(cursorSessionId); + } + + // Store session ID for future use and trigger refresh + if (cursorSessionId && !currentSessionId) { + setCurrentSessionId(cursorSessionId); + sessionStorage.removeItem('pendingSessionId'); + + // Trigger a project refresh to update the sidebar with the new session + if (window.refreshProjects) { + setTimeout(() => window.refreshProjects(), 500); + } + } + break; + + case 'cursor-output': + // Handle Cursor raw terminal output; strip ANSI and ignore empty control-only payloads + try { + const raw = String(latestMessage.data ?? ''); + const cleaned = raw.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '').replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '').trim(); + if (cleaned) { + streamBufferRef.current += (streamBufferRef.current ? `\n${cleaned}` : cleaned); + if (!streamTimerRef.current) { + streamTimerRef.current = setTimeout(() => { + const chunk = streamBufferRef.current; + streamBufferRef.current = ''; + streamTimerRef.current = null; + if (!chunk) return; + setChatMessages(prev => { + const updated = [...prev]; + const last = updated[updated.length - 1]; + if (last && last.type === 'assistant' && !last.isToolUse && last.isStreaming) { + last.content = last.content ? `${last.content}\n${chunk}` : chunk; + } else { + updated.push({ type: 'assistant', content: chunk, timestamp: new Date(), isStreaming: true }); + } + return updated; + }); + }, 100); + } + } + } catch (e) { + console.warn('Error handling cursor-output message:', e); + } + break; + case 'claude-complete': setIsLoading(false); setCanAbortSession(false); @@ -1624,6 +2308,11 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess if (pendingSessionId && !currentSessionId && latestMessage.exitCode === 0) { setCurrentSessionId(pendingSessionId); sessionStorage.removeItem('pendingSessionId'); + + // Trigger a project refresh to update the sidebar with the new session + if (window.refreshProjects) { + setTimeout(() => window.refreshProjects(), 500); + } } // Clear persisted chat messages after successful completion @@ -1652,7 +2341,6 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess case 'claude-status': // Handle Claude working status messages - console.log('🔔 Received claude-status message:', latestMessage); const statusData = latestMessage.data; if (statusData) { // Parse the status message to extract relevant information @@ -1683,7 +2371,6 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess statusInfo.can_interrupt = statusData.can_interrupt; } - console.log('📊 Setting claude status:', statusInfo); setClaudeStatus(statusInfo); setIsLoading(true); setCanAbortSession(statusInfo.can_interrupt); @@ -2017,20 +2704,21 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess setIsUserScrolledUp(false); // Reset scroll state so auto-scroll works for Claude's response setTimeout(() => scrollToBottom(), 100); // Longer delay to ensure message is rendered + // Determine effective session id for replies to avoid race on state updates + const effectiveSessionId = currentSessionId || selectedSession?.id || sessionStorage.getItem('cursorSessionId'); + // Session Protection: Mark session as active to prevent automatic project updates during conversation - // This is crucial for maintaining chat state integrity. We handle two cases: - // 1. Existing sessions: Use the real currentSessionId - // 2. New sessions: Generate temporary identifier "new-session-{timestamp}" since real ID comes via WebSocket later - // This ensures no gap in protection between message send and session creation - const sessionToActivate = currentSessionId || `new-session-${Date.now()}`; + // Use existing session if available; otherwise a temporary placeholder until backend provides real ID + const sessionToActivate = effectiveSessionId || `new-session-${Date.now()}`; if (onSessionActive) { onSessionActive(sessionToActivate); } - // Get tools settings from localStorage + // Get tools settings from localStorage based on provider const getToolsSettings = () => { try { - const savedSettings = safeLocalStorage.getItem('claude-tools-settings'); + const settingsKey = provider === 'cursor' ? 'cursor-tools-settings' : 'claude-tools-settings'; + const savedSettings = safeLocalStorage.getItem(settingsKey); if (savedSettings) { return JSON.parse(savedSettings); } @@ -2046,20 +2734,40 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess const toolsSettings = getToolsSettings(); - // Send command to Claude CLI via WebSocket with images - sendMessage({ - type: 'claude-command', - command: input, - options: { - projectPath: selectedProject.path, - cwd: selectedProject.fullPath, - sessionId: currentSessionId, - resume: !!currentSessionId, - toolsSettings: toolsSettings, - permissionMode: permissionMode, - images: uploadedImages // Pass images to backend - } - }); + // Send command based on provider + if (provider === 'cursor') { + // Send Cursor command (always use cursor-command; include resume/sessionId when replying) + sendMessage({ + type: 'cursor-command', + command: input, + sessionId: effectiveSessionId, + options: { + // Prefer fullPath (actual cwd for project), fallback to path + cwd: selectedProject.fullPath || selectedProject.path, + projectPath: selectedProject.fullPath || selectedProject.path, + sessionId: effectiveSessionId, + resume: !!effectiveSessionId, + model: cursorModel, + skipPermissions: toolsSettings?.skipPermissions || false, + toolsSettings: toolsSettings + } + }); + } else { + // Send Claude command (existing code) + sendMessage({ + type: 'claude-command', + command: input, + options: { + projectPath: selectedProject.path, + cwd: selectedProject.fullPath, + sessionId: currentSessionId, + resume: !!currentSessionId, + toolsSettings: toolsSettings, + permissionMode: permissionMode, + images: uploadedImages // Pass images to backend + } + }); + } setInput(''); setAttachedImages([]); @@ -2211,7 +2919,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess if (currentSessionId && canAbortSession) { sendMessage({ type: 'abort-session', - sessionId: currentSessionId + sessionId: currentSessionId, + provider: provider }); } }; @@ -2258,16 +2967,145 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
) : chatMessages.length === 0 ? (
-
-

Start a conversation with Claude

-

- Ask questions about your code, request changes, or get help with development tasks -

-
+ {!selectedSession && !currentSessionId && ( +
+

Choose Your AI Assistant

+

+ Select a provider to start a new conversation +

+ +
+ {/* Claude Button */} + + + {/* Cursor Button */} + +
+ + {/* Model Selection for Cursor - Always reserve space to prevent jumping */} +
+ + +
+ +

+ {provider === 'claude' + ? 'Ready to use Claude AI. Start typing your message below.' + : provider === 'cursor' + ? `Ready to use Cursor with ${cursorModel}. Start typing your message below.` + : 'Select a provider above to begin' + } +

+
+ )} + {selectedSession && ( +
+

Continue your conversation

+

+ Ask questions about your code, request changes, or get help with development tasks +

+
+ )}
) : ( <> - {chatMessages.length > visibleMessageCount && ( + {/* Loading indicator for older messages */} + {isLoadingMoreMessages && ( +
+
+
+

Loading older messages...

+
+
+ )} + + {/* Indicator showing there are more messages to load */} + {hasMoreMessages && !isLoadingMoreMessages && ( +
+ {totalMessages > 0 && ( + + Showing {sessionMessages.length} of {totalMessages} messages • + Scroll up to load more + + )} +
+ )} + + {/* Legacy message count indicator (for non-paginated view) */} + {!hasMoreMessages && chatMessages.length > visibleMessageCount && (
Showing last {visibleMessageCount} messages ({chatMessages.length} total) • )} -
- {activeTab === 'chat' && selectedSession ? ( -
-

- {selectedSession.summary} -

-
- {selectedProject.displayName} • {selectedSession.id} -
-
- ) : activeTab === 'chat' && !selectedSession ? ( -
-

- New Session -

-
- {selectedProject.displayName} -
-
- ) : ( -
-

- {activeTab === 'files' ? 'Project Files' : activeTab === 'git' ? 'Source Control' : 'Project'} -

-
- {selectedProject.displayName} -
+
+ {activeTab === 'chat' && selectedSession && ( +
+ {selectedSession.__provider === 'cursor' ? ( + + ) : ( + + )}
)} +
+ {activeTab === 'chat' && selectedSession ? ( +
+

+ {selectedSession.__provider === 'cursor' ? (selectedSession.name || 'Untitled Session') : (selectedSession.summary || 'New Session')} +

+
+ {selectedProject.displayName} • {selectedSession.id} +
+
+ ) : activeTab === 'chat' && !selectedSession ? ( +
+

+ New Session +

+
+ {selectedProject.displayName} +
+
+ ) : ( +
+

+ {activeTab === 'files' ? 'Project Files' : activeTab === 'git' ? 'Source Control' : 'Project'} +

+
+ {selectedProject.displayName} +
+
+ )} +
diff --git a/src/components/Shell.jsx b/src/components/Shell.jsx index 3033b03..03d2bfd 100644 --- a/src/components/Shell.jsx +++ b/src/components/Shell.jsx @@ -436,6 +436,7 @@ function Shell({ selectedProject, selectedSession, isActive }) { projectPath: selectedProject.fullPath || selectedProject.path, sessionId: selectedSession?.id, hasSession: !!selectedSession, + provider: selectedSession?.__provider || 'claude', cols: terminal.current.cols, rows: terminal.current.rows }; @@ -530,11 +531,16 @@ function Shell({ selectedProject, selectedSession, isActive }) {
- {selectedSession && ( - - ({selectedSession.summary.slice(0, 30)}...) - - )} + {selectedSession && (() => { + const displaySessionName = selectedSession.__provider === 'cursor' + ? (selectedSession.name || 'Untitled Session') + : (selectedSession.summary || 'New Session'); + return ( + + ({displaySessionName.slice(0, 30)}...) + + ); + })()} {!selectedSession && ( (New Session) )} @@ -601,7 +607,12 @@ function Shell({ selectedProject, selectedSession, isActive }) {

{selectedSession ? - `Resume session: ${selectedSession.summary.slice(0, 50)}...` : + (() => { + const displaySessionName = selectedSession.__provider === 'cursor' + ? (selectedSession.name || 'Untitled Session') + : (selectedSession.summary || 'New Session'); + return `Resume session: ${displaySessionName.slice(0, 50)}...`; + })() : 'Start a new Claude session' }

diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index 9a1e111..36f3bbf 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -7,6 +7,7 @@ import { Input } from './ui/input'; import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRight, Edit3, Check, X, Trash2, Settings, FolderPlus, RefreshCw, Sparkles, Edit2, Star, Search } from 'lucide-react'; import { cn } from '../lib/utils'; import ClaudeLogo from './ClaudeLogo'; +import CursorLogo from './CursorLogo.jsx'; import { api } from '../utils/api'; // Move formatTimeAgo outside component to avoid recreation on every render @@ -202,9 +203,12 @@ function Sidebar({ // Helper function to get all sessions for a project (initial + additional) const getAllSessions = (project) => { - const initialSessions = project.sessions || []; - const additional = additionalSessions[project.name] || []; - return [...initialSessions, ...additional]; + // Combine Claude and Cursor sessions; Sidebar will display icon per row + const claudeSessions = [...(project.sessions || []), ...(additionalSessions[project.name] || [])].map(s => ({ ...s, __provider: 'claude' })); + const cursorSessions = (project.cursorSessions || []).map(s => ({ ...s, __provider: 'cursor' })); + // Sort by most recent activity/date + const normalizeDate = (s) => new Date(s.__provider === 'cursor' ? s.createdAt : s.lastActivity); + return [...claudeSessions, ...cursorSessions].sort((a, b) => normalizeDate(b) - normalizeDate(a)); }; // Helper function to get the last activity date for a project @@ -979,11 +983,19 @@ function Sidebar({
) : ( getAllSessions(project).map((session) => { + // Handle both Claude and Cursor session formats + const isCursorSession = session.__provider === 'cursor'; + // Calculate if session is active (within last 10 minutes) - const sessionDate = new Date(session.lastActivity); + const sessionDate = new Date(isCursorSession ? session.createdAt : session.lastActivity); const diffInMinutes = Math.floor((currentTime - sessionDate) / (1000 * 60)); const isActive = diffInMinutes < 10; + // Get session display values + const sessionName = isCursorSession ? (session.name || 'Untitled Session') : (session.summary || 'New Session'); + const sessionTime = isCursorSession ? session.createdAt : session.lastActivity; + const messageCount = session.messageCount || 0; + return (
{/* Active session indicator dot */} @@ -1014,38 +1026,49 @@ function Sidebar({ "w-5 h-5 rounded-md flex items-center justify-center flex-shrink-0", selectedSession?.id === session.id ? "bg-primary/10" : "bg-muted/50" )}> - + {isCursorSession ? ( + + ) : ( + + )}
- {session.summary || 'New Session'} + {sessionName}
-
+
- {formatTimeAgo(session.lastActivity, currentTime)} + {formatTimeAgo(sessionTime, currentTime)} - {session.messageCount > 0 && ( + {messageCount > 0 && ( - {session.messageCount} + {messageCount} )} + {/* Provider tiny icon */} + + {isCursorSession ? ( + + ) : ( + + )} +
- {/* Mobile delete button */} - + {/* Mobile delete button - only for Claude sessions */} + {!isCursorSession && ( + + )}
@@ -1062,26 +1085,39 @@ function Sidebar({ onTouchEnd={handleTouchClick(() => onSessionSelect(session))} >
- + {isCursorSession ? ( + + ) : ( + + )}
- {session.summary || 'New Session'} + {sessionName}
- {formatTimeAgo(session.lastActivity, currentTime)} + {formatTimeAgo(sessionTime, currentTime)} - {session.messageCount > 0 && ( + {messageCount > 0 && ( - {session.messageCount} + {messageCount} )} + {/* Provider tiny icon */} + + {isCursorSession ? ( + + ) : ( + + )} +
- {/* Desktop hover buttons */} + {/* Desktop hover buttons - only for Claude sessions */} + {!isCursorSession && (
{editingSession === session.id ? ( <> @@ -1168,6 +1204,7 @@ function Sidebar({ )}
+ )}
); diff --git a/src/components/ToolsSettings.jsx b/src/components/ToolsSettings.jsx index 0c8eb0a..b0939c3 100644 --- a/src/components/ToolsSettings.jsx +++ b/src/components/ToolsSettings.jsx @@ -41,7 +41,16 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) { const [mcpToolsLoading, setMcpToolsLoading] = useState({}); const [activeTab, setActiveTab] = useState('tools'); const [jsonValidationError, setJsonValidationError] = useState(''); - // Common tool patterns + const [toolsProvider, setToolsProvider] = useState('claude'); // 'claude' or 'cursor' + + // Cursor-specific states + const [cursorAllowedCommands, setCursorAllowedCommands] = useState([]); + const [cursorDisallowedCommands, setCursorDisallowedCommands] = useState([]); + const [cursorSkipPermissions, setCursorSkipPermissions] = useState(false); + const [newCursorCommand, setNewCursorCommand] = useState(''); + const [newCursorDisallowedCommand, setNewCursorDisallowedCommand] = useState(''); + const [cursorMcpServers, setCursorMcpServers] = useState([]); + // Common tool patterns for Claude const commonTools = [ 'Bash(git log:*)', 'Bash(git diff:*)', @@ -58,7 +67,45 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) { 'WebFetch', 'WebSearch' ]; + + // Common shell commands for Cursor + const commonCursorCommands = [ + 'Shell(ls)', + 'Shell(mkdir)', + 'Shell(cd)', + 'Shell(cat)', + 'Shell(echo)', + 'Shell(git status)', + 'Shell(git diff)', + 'Shell(git log)', + 'Shell(npm install)', + 'Shell(npm run)', + 'Shell(python)', + 'Shell(node)' + ]; + // Fetch Cursor MCP servers + const fetchCursorMcpServers = async () => { + try { + const token = localStorage.getItem('auth-token'); + const response = await fetch('/api/cursor/mcp', { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + const data = await response.json(); + setCursorMcpServers(data.servers || []); + } else { + console.error('Failed to fetch Cursor MCP servers'); + } + } catch (error) { + console.error('Error fetching Cursor MCP servers:', error); + } + }; + // MCP API functions const fetchMcpServers = async () => { try { @@ -268,7 +315,7 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) { const loadSettings = async () => { try { - // Load from localStorage + // Load Claude settings from localStorage const savedSettings = localStorage.getItem('claude-tools-settings'); if (savedSettings) { @@ -284,9 +331,27 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) { setSkipPermissions(false); setProjectSortOrder('name'); } + + // Load Cursor settings from localStorage + const savedCursorSettings = localStorage.getItem('cursor-tools-settings'); + + if (savedCursorSettings) { + const cursorSettings = JSON.parse(savedCursorSettings); + setCursorAllowedCommands(cursorSettings.allowedCommands || []); + setCursorDisallowedCommands(cursorSettings.disallowedCommands || []); + setCursorSkipPermissions(cursorSettings.skipPermissions || false); + } else { + // Set Cursor defaults + setCursorAllowedCommands([]); + setCursorDisallowedCommands([]); + setCursorSkipPermissions(false); + } // Load MCP servers from API await fetchMcpServers(); + + // Load Cursor MCP servers + await fetchCursorMcpServers(); } catch (error) { console.error('Error loading tool settings:', error); // Set defaults on error @@ -302,7 +367,8 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) { setSaveStatus(null); try { - const settings = { + // Save Claude settings + const claudeSettings = { allowedTools, disallowedTools, skipPermissions, @@ -310,9 +376,17 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) { lastUpdated: new Date().toISOString() }; + // Save Cursor settings + const cursorSettings = { + allowedCommands: cursorAllowedCommands, + disallowedCommands: cursorDisallowedCommands, + skipPermissions: cursorSkipPermissions, + lastUpdated: new Date().toISOString() + }; // Save to localStorage - localStorage.setItem('claude-tools-settings', JSON.stringify(settings)); + localStorage.setItem('claude-tools-settings', JSON.stringify(claudeSettings)); + localStorage.setItem('cursor-tools-settings', JSON.stringify(cursorSettings)); setSaveStatus('success'); @@ -635,6 +709,36 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) { {activeTab === 'tools' && (
+ {/* Provider Tabs */} +
+
+ + +
+
+ + {/* Claude Tools Content */} + {toolsProvider === 'claude' && ( +
+ {/* Skip Permissions */}
@@ -1360,6 +1464,216 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) { )}
)} + + {/* Cursor Tools Content */} + {toolsProvider === 'cursor' && ( +
+ + {/* Skip Permissions for Cursor */} +
+
+ +

+ Cursor Permission Settings +

+
+
+ +
+
+ + {/* Allowed Shell Commands */} +
+
+ +

+ Allowed Shell Commands +

+
+

+ Shell commands that are automatically allowed without prompting for permission +

+ +
+ setNewCursorCommand(e.target.value)} + placeholder='e.g., "Shell(ls)" or "Shell(git status)"' + onKeyPress={(e) => { + if (e.key === 'Enter') { + if (newCursorCommand && !cursorAllowedCommands.includes(newCursorCommand)) { + setCursorAllowedCommands([...cursorAllowedCommands, newCursorCommand]); + setNewCursorCommand(''); + } + } + }} + className="flex-1 h-10 touch-manipulation" + style={{ fontSize: '16px' }} + /> + +
+ + {/* Common commands quick add */} +
+

+ Quick add common commands: +

+
+ {commonCursorCommands.map(cmd => ( + + ))} +
+
+ +
+ {cursorAllowedCommands.map(cmd => ( +
+ + {cmd} + + +
+ ))} + {cursorAllowedCommands.length === 0 && ( +
+ No allowed shell commands configured +
+ )} +
+
+ + {/* Disallowed Shell Commands */} +
+
+ +

+ Disallowed Shell Commands +

+
+

+ Shell commands that should always be denied +

+ +
+ setNewCursorDisallowedCommand(e.target.value)} + placeholder='e.g., "Shell(rm -rf)" or "Shell(sudo)"' + onKeyPress={(e) => { + if (e.key === 'Enter') { + if (newCursorDisallowedCommand && !cursorDisallowedCommands.includes(newCursorDisallowedCommand)) { + setCursorDisallowedCommands([...cursorDisallowedCommands, newCursorDisallowedCommand]); + setNewCursorDisallowedCommand(''); + } + } + }} + className="flex-1 h-10 touch-manipulation" + style={{ fontSize: '16px' }} + /> + +
+ +
+ {cursorDisallowedCommands.map(cmd => ( +
+ + {cmd} + + +
+ ))} + {cursorDisallowedCommands.length === 0 && ( +
+ No disallowed shell commands configured +
+ )} +
+
+ + {/* Help Section */} +
+

+ Cursor Shell Command Examples: +

+
    +
  • "Shell(ls)" - Allow ls command
  • +
  • "Shell(git status)" - Allow git status command
  • +
  • "Shell(mkdir)" - Allow mkdir command
  • +
  • "-f" flag - Skip all permission prompts (dangerous)
  • +
+
+
+ )} +
+ )}
diff --git a/src/utils/api.js b/src/utils/api.js index 49ae915..2297851 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -43,8 +43,16 @@ export const api = { projects: () => authenticatedFetch('/api/projects'), sessions: (projectName, limit = 5, offset = 0) => authenticatedFetch(`/api/projects/${projectName}/sessions?limit=${limit}&offset=${offset}`), - sessionMessages: (projectName, sessionId) => - authenticatedFetch(`/api/projects/${projectName}/sessions/${sessionId}/messages`), + sessionMessages: (projectName, sessionId, limit = null, offset = 0) => { + const params = new URLSearchParams(); + if (limit !== null) { + params.append('limit', limit); + params.append('offset', offset); + } + const queryString = params.toString(); + const url = `/api/projects/${projectName}/sessions/${sessionId}/messages${queryString ? `?${queryString}` : ''}`; + return authenticatedFetch(url); + }, renameProject: (projectName, displayName) => authenticatedFetch(`/api/projects/${projectName}/rename`, { method: 'PUT', diff --git a/store.db-shm b/store.db-shm new file mode 100644 index 0000000..fe9ac28 Binary files /dev/null and b/store.db-shm differ diff --git a/store.db-wal b/store.db-wal new file mode 100644 index 0000000..e69de29 diff --git a/test.html b/test.html new file mode 100644 index 0000000..bdb58d2 --- /dev/null +++ b/test.html @@ -0,0 +1 @@ +hello world 5