diff --git a/.gitignore b/.gitignore index da240bd7..1f06d755 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,6 @@ mcp/bundle/ harnesses/pi/bundle/ evals/ .cursor/ -cursor_hivemind_environment_variables_c.md \ No newline at end of file +cursor_hivemind_environment_variables_c.md +cursor-extension/node_modules/ +cursor-extension/dist/ \ No newline at end of file diff --git a/cursor-extension/.vscodeignore b/cursor-extension/.vscodeignore new file mode 100644 index 00000000..6da893c3 --- /dev/null +++ b/cursor-extension/.vscodeignore @@ -0,0 +1,9 @@ +.vscode/** +.vscode-test/** +src/** +node_modules/** +**/*.map +**/*.ts +webpack.config.js +tsconfig.json +.gitignore diff --git a/cursor-extension/README.md b/cursor-extension/README.md new file mode 100644 index 00000000..22eb8522 --- /dev/null +++ b/cursor-extension/README.md @@ -0,0 +1,27 @@ +# Hivemind for Cursor + +VS Code / Cursor extension that surfaces Hivemind health, authentication, dashboard KPIs, codebase graph, rules, and skill sync inside the editor. + +## Development + +```bash +cd cursor-extension +npm install +npm run watch # or npm run compile +``` + +Press F5 in VS Code with the extension folder open, or install the VSIX after packaging. + +## Features + +- Status bar health indicator (CLI, cursor-agent, hooks, login) +- One-click hook auto-wiring to `~/.cursor/hooks.json` +- Browser and API-key Hivemind login +- Dashboard webview: KPIs, settings, sessions, graph, rules, skills +- Cursor skill symlink bridge (`~/.cursor/skills-cursor/` and project `.cursor/skills/`) + +## Requirements + +- Hivemind CLI on PATH +- `cursor-agent` for session summaries +- Built hivemind bundle at `harnesses/cursor/bundle/` (run `npm run build` in repo root) diff --git a/cursor-extension/media/icon.svg b/cursor-extension/media/icon.svg new file mode 100644 index 00000000..63b86384 --- /dev/null +++ b/cursor-extension/media/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/cursor-extension/package-lock.json b/cursor-extension/package-lock.json new file mode 100644 index 00000000..f993759e --- /dev/null +++ b/cursor-extension/package-lock.json @@ -0,0 +1,1693 @@ +{ + "name": "hivemind-cursor-extension", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hivemind-cursor-extension", + "version": "0.1.0", + "devDependencies": { + "@types/node": "^20.11.0", + "@types/vscode": "^1.85.0", + "ts-loader": "^9.5.1", + "typescript": "^5.4.0", + "webpack": "^5.90.0", + "webpack-cli": "^5.1.4" + }, + "engines": { + "vscode": "^1.85.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.120.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.120.0.tgz", + "integrity": "sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.37", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", + "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.372", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz", + "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.0.tgz", + "integrity": "sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-loader": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.6.0.tgz", + "integrity": "sha512-dsJO0S+T7grTDWTc4a0nTygXGjKncVUpx8Y+af8EvI/D5WgTJby5UEk5eoMCB9EcLQmnvitqh99MqtjtHgAwFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "loader-utils": "*", + "typescript": "*", + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "loader-utils": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.107.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.2.tgz", + "integrity": "sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.22.0", + "es-module-lexer": "^2.1.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.2", + "mime-db": "^1.54.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.5.0", + "watchpack": "^2.5.1", + "webpack-sources": "^3.5.0" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", + "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/cursor-extension/package.json b/cursor-extension/package.json new file mode 100644 index 00000000..28eb198e --- /dev/null +++ b/cursor-extension/package.json @@ -0,0 +1,81 @@ +{ + "name": "hivemind-cursor-extension", + "displayName": "Hivemind for Cursor", + "description": "Shared memory, health checks, dashboard, and codebase graph inside Cursor", + "version": "0.1.0", + "publisher": "deeplake", + "engines": { + "vscode": "^1.85.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "onStartupFinished" + ], + "main": "./dist/extension.js", + "contributes": { + "commands": [ + { + "command": "hivemind.runOnboarding", + "title": "Hivemind: Run Onboarding" + }, + { + "command": "hivemind.login", + "title": "Hivemind: Log In" + }, + { + "command": "hivemind.logout", + "title": "Hivemind: Log Out" + }, + { + "command": "hivemind.showStatus", + "title": "Hivemind: Show Status" + }, + { + "command": "hivemind.wireHooks", + "title": "Hivemind: Wire / Refresh Hooks" + }, + { + "command": "hivemind.openLogs", + "title": "Hivemind: Open Logs" + }, + { + "command": "hivemind.openDashboard", + "title": "Hivemind: Open Dashboard" + } + ], + "viewsContainers": { + "activitybar": [ + { + "id": "hivemind", + "title": "Hivemind", + "icon": "media/icon.svg" + } + ] + }, + "views": { + "hivemind": [ + { + "id": "hivemind.dashboard", + "name": "Dashboard", + "type": "webview" + } + ] + } + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "webpack --mode production", + "watch": "webpack --mode development --watch", + "lint": "tsc --noEmit" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "@types/vscode": "^1.85.0", + "ts-loader": "^9.5.1", + "typescript": "^5.4.0", + "webpack": "^5.90.0", + "webpack-cli": "^5.1.4" + } +} diff --git a/cursor-extension/scripts/load-dashboard.mjs b/cursor-extension/scripts/load-dashboard.mjs new file mode 100644 index 00000000..59e2ad4e --- /dev/null +++ b/cursor-extension/scripts/load-dashboard.mjs @@ -0,0 +1,6 @@ +import { loadDashboardData } from "../../src/dashboard/data.js"; +import { readUsageRecords } from "../../src/notifications/usage-tracker.js"; + +const cwd = process.argv[2] || process.cwd(); +const data = await loadDashboardData({ cwd }); +console.log(JSON.stringify(data)); diff --git a/cursor-extension/scripts/load-sessions.mjs b/cursor-extension/scripts/load-sessions.mjs new file mode 100644 index 00000000..2291f679 --- /dev/null +++ b/cursor-extension/scripts/load-sessions.mjs @@ -0,0 +1,9 @@ +import { readUsageRecords } from "../../src/notifications/usage-tracker.js"; + +const cwd = process.argv[2] || process.cwd(); +const records = readUsageRecords().slice(-20).reverse(); +console.log(JSON.stringify(records.map((r) => ({ + sessionId: r.sessionId, + endedAt: r.endedAt, + memorySearchCount: r.memorySearchCount, +})))); diff --git a/cursor-extension/src/auth/api-key.ts b/cursor-extension/src/auth/api-key.ts new file mode 100644 index 00000000..b7a78174 --- /dev/null +++ b/cursor-extension/src/auth/api-key.ts @@ -0,0 +1,62 @@ +import * as vscode from "vscode"; +import { saveCredentialsFromToken } from "./device-flow"; +import { loadStoredCredentials } from "./detector"; +import { logSafe } from "../utils/output"; + +const DEFAULT_API_URL = "https://api.deeplake.ai"; + +export async function loginApiKey(): Promise { + const token = await vscode.window.showInputBox({ + prompt: "Enter your Hivemind API token", + password: true, + ignoreFocusOut: true, + placeHolder: "Paste token (never logged or stored in settings)", + }); + if (!token) return false; + + const apiUrl = process.env.HIVEMIND_API_URL ?? DEFAULT_API_URL; + try { + await saveCredentialsFromToken(token.trim(), apiUrl); + logSafe("Hivemind API key login succeeded."); + return true; + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : "Invalid token"; + await vscode.window.showErrorMessage(`Login failed: ${msg}`); + return false; + } +} + +export async function promptLoginMethod(): Promise { + const choice = await vscode.window.showQuickPick( + [ + { label: "Browser sign-in (device flow)", id: "browser" }, + { label: "API key", id: "apikey" }, + { label: "Terminal (hivemind login)", id: "cli" }, + ], + { placeHolder: "Choose Hivemind login method" }, + ); + if (!choice) return false; + + if (choice.id === "browser") { + const { loginBrowserFlow } = await import("./device-flow"); + try { + await loginBrowserFlow(); + return true; + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : "Login failed"; + await vscode.window.showErrorMessage(msg); + return false; + } + } + if (choice.id === "apikey") { + return loginApiKey(); + } + const { loginViaHivemindCli } = await import("./device-flow"); + return loginViaHivemindCli(); +} + +export function getActiveCredentialsSummary(): string | undefined { + const creds = loadStoredCredentials(); + if (!creds) return undefined; + return creds.userName ?? creds.orgName ?? "Logged in"; +} diff --git a/cursor-extension/src/auth/detector.ts b/cursor-extension/src/auth/detector.ts new file mode 100644 index 00000000..c30022c4 --- /dev/null +++ b/cursor-extension/src/auth/detector.ts @@ -0,0 +1,88 @@ +import { existsSync } from "node:fs"; +import type { AuthState } from "../types/health"; +import { credentialsPath } from "../utils/paths"; +import { readJson } from "../utils/fs-json"; +import { runHealthCheck } from "../health/checker"; +import { sanitizeApiUrl } from "./safe-url"; + +export interface StoredCredentials { + token: string; + orgId: string; + orgName?: string; + userName?: string; + workspaceId?: string; + apiUrl?: string; + savedAt: string; +} + +const DEFAULT_API_URL = "https://api.deeplake.ai"; + +export function loadStoredCredentials(): StoredCredentials | null { + return readJson(credentialsPath()); +} + +export function isCredentialFilePresent(): boolean { + return existsSync(credentialsPath()) && loadStoredCredentials() !== null; +} + +async function validateCredentialsOnline(creds: StoredCredentials): Promise { + const apiUrl = sanitizeApiUrl(creds.apiUrl, DEFAULT_API_URL); + try { + const resp = await fetch(`${apiUrl}/me`, { + headers: { + Authorization: `Bearer ${creds.token}`, + "Content-Type": "application/json", + }, + signal: AbortSignal.timeout(8000), + }); + return resp.ok; + } catch { + return false; + } +} + +export async function detectAuthState(): Promise { + const creds = loadStoredCredentials(); + const health = await runHealthCheck(); + const d3 = health.dimensions.find((d) => d.id === "d3"); + const cursorAgentLoggedIn = d3?.status === "ok"; + const cursorAgentMessage = d3?.message; + + if (!creds) { + return { + state: "logged_out", + cursorAgentLoggedIn, + cursorAgentMessage, + }; + } + + const identity = creds.userName ?? creds.orgName ?? creds.orgId; + const online = await validateCredentialsOnline(creds); + if (!online) { + return { + state: "unknown_offline", + identity, + orgName: creds.orgName, + workspaceId: creds.workspaceId, + cursorAgentLoggedIn, + cursorAgentMessage, + }; + } + + return { + state: "logged_in", + identity, + orgName: creds.orgName, + workspaceId: creds.workspaceId, + cursorAgentLoggedIn, + cursorAgentMessage, + }; +} + +export function formatIdentity(auth: AuthState): string { + if (!auth.identity) return "Not logged in"; + const parts = [auth.identity]; + if (auth.orgName) parts.push(`org: ${auth.orgName}`); + if (auth.workspaceId) parts.push(`workspace: ${auth.workspaceId}`); + return parts.join(" · "); +} diff --git a/cursor-extension/src/auth/device-flow.ts b/cursor-extension/src/auth/device-flow.ts new file mode 100644 index 00000000..0ac2ea24 --- /dev/null +++ b/cursor-extension/src/auth/device-flow.ts @@ -0,0 +1,147 @@ +import { execFileSync } from "node:child_process"; +import * as vscode from "vscode"; +import { loadStoredCredentials, type StoredCredentials } from "./detector"; +import { credentialsPath, deeplakeConfigDir } from "../utils/paths"; +import { logSafe } from "../utils/output"; +import { assertSafeCredentialFields, openExternalUrl, sanitizeApiUrl } from "./safe-url"; + +const DEFAULT_API_URL = "https://api.deeplake.ai"; + +interface DeviceCodeResponse { + device_code: string; + user_code: string; + verification_uri: string; + verification_uri_complete: string; + expires_in: number; + interval: number; +} + +interface DeviceTokenResponse { + access_token: string; + token_type: string; + expires_in: number; +} + +async function requestDeviceCode(apiUrl: string): Promise { + const base = sanitizeApiUrl(apiUrl, DEFAULT_API_URL); + const resp = await fetch(`${base}/auth/device/code`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + }); + if (!resp.ok) throw new Error(`Device flow unavailable: HTTP ${resp.status}`); + return resp.json() as Promise; +} + +async function pollForToken(deviceCode: string, apiUrl: string): Promise { + const base = sanitizeApiUrl(apiUrl, DEFAULT_API_URL); + const resp = await fetch(`${base}/auth/device/token`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ device_code: deviceCode }), + }); + if (resp.ok) return resp.json() as Promise; + if (resp.status === 400) { + const err = (await resp.json().catch(() => null)) as { error?: string } | null; + if (err?.error === "authorization_pending" || err?.error === "slow_down") return null; + if (err?.error === "expired_token") throw new Error("Device code expired. Try again."); + if (err?.error === "access_denied") throw new Error("Authorization denied."); + } + throw new Error(`Token polling failed: HTTP ${resp.status}`); +} + +async function apiGet(path: string, token: string, apiUrl: string): Promise { + const base = sanitizeApiUrl(apiUrl, DEFAULT_API_URL); + const resp = await fetch(`${base}${path}`, { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + if (!resp.ok) throw new Error(`API ${resp.status}`); + return resp.json(); +} + +async function apiPost(path: string, body: unknown, token: string, apiUrl: string): Promise { + const base = sanitizeApiUrl(apiUrl, DEFAULT_API_URL); + const resp = await fetch(`${base}${path}`, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + if (!resp.ok) throw new Error(`API ${resp.status}`); + return resp.json(); +} + +export async function saveCredentialsFromToken(token: string, apiUrl: string): Promise { + const safeApiUrl = sanitizeApiUrl(apiUrl, DEFAULT_API_URL); + const user = (await apiGet("/me", token, safeApiUrl)) as { id: string; name: string; email?: string }; + const userName = user.name || (user.email ? user.email.split("@")[0] : "unknown"); + const orgs = (await apiGet("/organizations", token, safeApiUrl)) as { id: string; name: string }[]; + if (!Array.isArray(orgs) || orgs.length === 0) throw new Error("No organizations found for this account."); + const org = orgs[0]; + const tokenData = (await apiPost( + "/users/me/tokens", + { name: `hivemind-extension-${Date.now()}`, duration: 365 * 24 * 3600, organization_id: org.id }, + token, + safeApiUrl, + )) as { token: { token: string } }; + + const creds: StoredCredentials = { + token: tokenData.token.token, + orgId: org.id, + orgName: org.name, + userName, + apiUrl: safeApiUrl, + savedAt: new Date().toISOString(), + }; + + assertSafeCredentialFields(creds); + + const { mkdirSync, writeFileSync } = await import("node:fs"); + mkdirSync(deeplakeConfigDir(), { recursive: true, mode: 0o700 }); + writeFileSync(credentialsPath(), JSON.stringify(creds, null, 2), { mode: 0o600 }); + return creds; +} + +export async function loginBrowserFlow(): Promise { + const apiUrl = sanitizeApiUrl(process.env.HIVEMIND_API_URL, DEFAULT_API_URL); + const code = await requestDeviceCode(apiUrl); + await openExternalUrl(code.verification_uri_complete); + await vscode.window.showInformationMessage( + `Complete sign-in in your browser. Code: ${code.user_code}`, + "Open browser again", + ).then(async (choice) => { + if (choice === "Open browser again") await openExternalUrl(code.verification_uri_complete); + }); + + const interval = Math.max(code.interval || 5, 5) * 1000; + const deadline = Date.now() + code.expires_in * 1000; + + return await vscode.window.withProgress( + { location: vscode.ProgressLocation.Notification, title: "Waiting for Hivemind sign-in…", cancellable: true }, + async (_progress, cancelToken) => { + while (Date.now() < deadline) { + if (cancelToken.isCancellationRequested) throw new Error("Login cancelled."); + await new Promise((r) => setTimeout(r, interval)); + const result = await pollForToken(code.device_code, apiUrl); + if (result) { + logSafe("Hivemind browser login succeeded."); + return saveCredentialsFromToken(result.access_token, apiUrl); + } + } + throw new Error("Device code expired."); + }, + ); +} + +export async function loginViaHivemindCli(): Promise { + try { + execFileSync("hivemind", ["login"], { stdio: "inherit", timeout: 300000 }); + return loadStoredCredentials() !== null; + } catch { + return false; + } +} diff --git a/cursor-extension/src/auth/index.ts b/cursor-extension/src/auth/index.ts new file mode 100644 index 00000000..719cfb58 --- /dev/null +++ b/cursor-extension/src/auth/index.ts @@ -0,0 +1,4 @@ +export { detectAuthState, loadStoredCredentials, isCredentialFilePresent, formatIdentity } from "./detector"; +export { loginBrowserFlow, loginViaHivemindCli } from "./device-flow"; +export { loginApiKey, promptLoginMethod, getActiveCredentialsSummary } from "./api-key"; +export { logout } from "./logout"; diff --git a/cursor-extension/src/auth/logout.ts b/cursor-extension/src/auth/logout.ts new file mode 100644 index 00000000..ff1d9a68 --- /dev/null +++ b/cursor-extension/src/auth/logout.ts @@ -0,0 +1,21 @@ +import { unlinkSync } from "node:fs"; +import * as vscode from "vscode"; +import { credentialsPath } from "../utils/paths"; +import { logSafe } from "../utils/output"; + +export async function logout(): Promise { + let removed = false; + try { + unlinkSync(credentialsPath()); + removed = true; + } catch { + removed = false; + } + + const message = removed + ? "Hivemind credentials cleared from ~/.deeplake/credentials.json. Hooks remain installed; shared memory is inactive until you log in again." + : "No credentials file found to remove. Hooks remain installed."; + + logSafe(message); + await vscode.window.showInformationMessage(message); +} diff --git a/cursor-extension/src/auth/safe-url.ts b/cursor-extension/src/auth/safe-url.ts new file mode 100644 index 00000000..1e8da5db --- /dev/null +++ b/cursor-extension/src/auth/safe-url.ts @@ -0,0 +1,59 @@ +import * as vscode from "vscode"; + +const ALLOWED_AUTH_HOSTS = new Set([ + "api.deeplake.ai", + "app.deeplake.ai", + "auth.deeplake.ai", +]); + +/** Validate HTTPS URLs from the device-flow API before opening externally. */ +export function assertSafeExternalUrl(raw: string): URL { + let parsed: URL; + try { + parsed = new URL(raw); + } catch { + throw new Error("Invalid verification URL from auth server."); + } + if (parsed.protocol !== "https:") { + throw new Error("Verification URL must use HTTPS."); + } + if (!ALLOWED_AUTH_HOSTS.has(parsed.hostname)) { + throw new Error(`Unexpected auth host: ${parsed.hostname}`); + } + return parsed; +} + +export async function openExternalUrl(raw: string): Promise { + try { + const parsed = assertSafeExternalUrl(raw); + return vscode.env.openExternal(vscode.Uri.parse(parsed.toString())); + } catch { + return false; + } +} + +export function sanitizeApiUrl(raw: string | undefined, fallback: string): string { + const candidate = raw ?? fallback; + const parsed = assertSafeExternalUrl(candidate.endsWith("/") ? candidate.slice(0, -1) : candidate); + return parsed.origin; +} + +const TOKENish = /^[\x21-\x7E]{8,4096}$/; + +export function assertSafeCredentialFields(fields: { + token: string; + orgId: string; + orgName?: string; + userName?: string; + apiUrl?: string; +}): void { + if (!TOKENish.test(fields.token)) throw new Error("Invalid token shape from auth server."); + if (!/^[a-zA-Z0-9_-]{1,128}$/.test(fields.orgId)) throw new Error("Invalid org id from auth server."); + if (fields.userName && !/^[a-zA-Z0-9._-]{1,128}$/.test(fields.userName)) { + throw new Error("Invalid user name from auth server."); + } + if (fields.orgName && fields.orgName.length > 256) { + throw new Error("Invalid org name from auth server."); + } + if (fields.apiUrl) sanitizeApiUrl(fields.apiUrl, fields.apiUrl); +} diff --git a/cursor-extension/src/bridge/auto-sync.ts b/cursor-extension/src/bridge/auto-sync.ts new file mode 100644 index 00000000..0277075c --- /dev/null +++ b/cursor-extension/src/bridge/auto-sync.ts @@ -0,0 +1,44 @@ +import { runHivemindCli } from "../webview/data-bridge"; +import { logSafe } from "../utils/output"; +import { backfillCursorLinks, syncSkillsToCursor } from "./skill-sync"; + +/** + * Run skill pull fan-out and Cursor symlink sync on extension activation. + * Respects HIVEMIND_AUTOPULL_DISABLED (same contract as SessionStart auto-pull). + */ +export async function runAutoSyncOnActivation(projectRoot?: string): Promise { + if (process.env.HIVEMIND_AUTOPULL_DISABLED === "1") { + logSafe("Auto skill sync skipped (HIVEMIND_AUTOPULL_DISABLED=1)."); + return; + } + + const cwd = projectRoot ?? process.cwd(); + try { + const pullResult = await runHivemindCli( + ["skillify", "pull", "--all-users", "--to", "global"], + cwd, + ); + if (pullResult.ok) { + logSafe("Auto skill pull completed."); + } + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + logSafe(`Auto-pull skipped: ${msg}`); + } + + try { + const backfilled = backfillCursorLinks(projectRoot); + if (backfilled > 0) { + logSafe(`Backfilled Cursor links for ${backfilled} manifest entries.`); + } + const state = syncSkillsToCursor(projectRoot); + if (state.erroredCount > 0) { + logSafe( + `Cursor skill sync: ${state.syncedCount} synced, ${state.skippedCount} partial, ${state.erroredCount} failed.`, + ); + } + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + logSafe(`Cursor skill sync failed: ${msg}`); + } +} diff --git a/cursor-extension/src/bridge/skill-sync.ts b/cursor-extension/src/bridge/skill-sync.ts new file mode 100644 index 00000000..8f87861e --- /dev/null +++ b/cursor-extension/src/bridge/skill-sync.ts @@ -0,0 +1,250 @@ +import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, renameSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { basename, dirname, join } from "node:path"; +import * as vscode from "vscode"; +import type { SkillSyncResult, SkillSyncState } from "../types/health"; + +interface PulledEntry { + dirName: string; + install: "global" | "project"; + installRoot: string; + symlinks: string[]; +} + +interface PulledManifest { + version: number; + entries: PulledEntry[]; +} + +function canonicalSkillsRoot(): string { + return join(homedir(), ".claude", "skills"); +} + +function skillifyStateDir(): string { + const override = process.env.HIVEMIND_STATE_DIR?.trim(); + return override && override.length > 0 ? override : join(homedir(), ".deeplake", "state", "skillify"); +} + +function manifestPath(): string { + return join(skillifyStateDir(), "pulled.json"); +} + +function cursorInstalled(home: string = homedir()): boolean { + return existsSync(join(home, ".cursor")); +} + +export function detectCursorSkillsRoots(projectRoot?: string, home: string = homedir()): string[] { + if (!cursorInstalled(home)) return []; + const roots = [join(home, ".cursor", "skills-cursor")]; + if (projectRoot) roots.push(join(projectRoot, ".cursor", "skills")); + return roots; +} + +function fanOutSymlinks(canonicalDir: string, dirName: string, agentRoots: string[]): string[] { + const out: string[] = []; + for (const root of agentRoots) { + const link = join(root, dirName); + let existing; + try { + existing = lstatSync(link); + } catch { + existing = null; + } + if (existing) { + if (!existing.isSymbolicLink()) continue; + let current: string | null; + try { + current = readlinkSync(link); + } catch { + current = null; + } + if (current === canonicalDir) { + out.push(link); + continue; + } + try { + unlinkSync(link); + } catch { + continue; + } + } + try { + mkdirSync(dirname(link), { recursive: true }); + symlinkSync(canonicalDir, link, "dir"); + out.push(link); + } catch { + /* best-effort */ + } + } + return out; +} + +function listCanonicalSkillDirs(skillsRoot: string): string[] { + if (!existsSync(skillsRoot)) return []; + return readdirSync(skillsRoot).filter((name) => { + if (!name.includes("--")) return false; + try { + return lstatSync(join(skillsRoot, name)).isDirectory(); + } catch { + return false; + } + }); +} + +function loadManifest(): PulledManifest { + const path = manifestPath(); + if (!existsSync(path)) return { version: 1, entries: [] }; + try { + const parsed = JSON.parse(readFileSync(path, "utf-8")) as PulledManifest; + if (!Array.isArray(parsed.entries)) return { version: 1, entries: [] }; + return parsed; + } catch { + return { version: 1, entries: [] }; + } +} + +function writeManifest(manifest: PulledManifest): void { + const path = manifestPath(); + mkdirSync(dirname(path), { recursive: true }); + const tmp = `${path}.tmp.${process.pid}`; + writeFileSync(tmp, JSON.stringify(manifest, null, 2)); + try { + unlinkSync(path); + } catch { + /* may not exist */ + } + try { + renameSync(tmp, path); + } catch { + writeFileSync(path, JSON.stringify(manifest, null, 2)); + } +} + +function mergeSymlinks(entry: PulledEntry, fresh: string[]): void { + const merged = [...new Set([...entry.symlinks, ...fresh])].sort(); + const prior = [...entry.symlinks].sort(); + if (merged.length === prior.length && merged.every((v, i) => v === prior[i])) return; + const manifest = loadManifest(); + const idx = manifest.entries.findIndex( + (e) => e.dirName === entry.dirName && e.installRoot === entry.installRoot, + ); + if (idx >= 0) manifest.entries[idx] = { ...entry, symlinks: merged }; + try { + writeManifest(manifest); + } catch { + /* best-effort */ + } +} + +/** Sync canonical pulled skills into Cursor global and project skill directories. */ +export function syncSkillsToCursor(projectRoot?: string): SkillSyncState { + const skillsRoot = canonicalSkillsRoot(); + const roots = detectCursorSkillsRoots(projectRoot); + const results: SkillSyncResult[] = []; + const dirs = listCanonicalSkillDirs(skillsRoot); + + if (roots.length === 0) { + return { + lastSyncAt: new Date().toISOString(), + results: dirs.map((dirName) => ({ + skillName: dirName, + status: "skipped", + reason: "Cursor not detected or no Cursor skill roots", + })), + syncedCount: 0, + skippedCount: dirs.length, + erroredCount: 0, + }; + } + + let synced = 0; + let skipped = 0; + let errored = 0; + + for (const dirName of dirs) { + const canonicalDir = join(skillsRoot, dirName); + const links = fanOutSymlinks(canonicalDir, dirName, roots); + if (links.length === 0) { + errored++; + results.push({ + skillName: dirName, + status: "errored", + reason: "Could not create Cursor symlinks (conflict or permission)", + }); + continue; + } + if (links.length < roots.length) { + skipped++; + results.push({ + skillName: dirName, + status: "skipped", + path: links[0], + reason: `Synced to ${links.length}/${roots.length} Cursor roots`, + }); + } else { + synced++; + results.push({ + skillName: dirName, + status: "synced", + path: links.join(", "), + }); + } + } + + return { + lastSyncAt: new Date().toISOString(), + results, + syncedCount: synced, + skippedCount: skipped, + erroredCount: errored, + }; +} + +/** Backfill Cursor symlinks for skills already recorded in the pull manifest. */ +export function backfillCursorLinks(projectRoot?: string): number { + const manifest = loadManifest(); + const cursorRoots = detectCursorSkillsRoots(projectRoot); + if (cursorRoots.length === 0) return 0; + + let updated = 0; + for (const entry of manifest.entries) { + const canonical = join(entry.installRoot, entry.dirName); + if (!existsSync(canonical)) continue; + const roots = + entry.install === "project" && projectRoot + ? cursorRoots.filter((p) => p.startsWith(projectRoot)) + : cursorRoots.filter((p) => p.includes("skills-cursor")); + const fresh = fanOutSymlinks(canonical, entry.dirName, roots); + if (fresh.length === 0) continue; + mergeSymlinks(entry, fresh); + updated++; + } + return updated; +} + +/** List local skill directory names available for promotion UI. */ +export function listLocalSkillsForPromoter(): Array<{ dirName: string; scope: "global" | "project"; path: string }> { + const out: Array<{ dirName: string; scope: "global" | "project"; path: string }> = []; + const globalRoot = canonicalSkillsRoot(); + for (const dirName of listCanonicalSkillDirs(globalRoot)) { + out.push({ dirName, scope: "global", path: join(globalRoot, dirName) }); + } + const workspace = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if (workspace) { + const projectRoot = join(workspace, ".claude", "skills"); + for (const dirName of listCanonicalSkillDirs(projectRoot)) { + out.push({ dirName, scope: "project", path: join(projectRoot, dirName) }); + } + } + return out; +} + +export function skillDirLabel(dirName: string): string { + const idx = dirName.lastIndexOf("--"); + if (idx <= 0) return dirName; + return `${dirName.slice(0, idx)} (${dirName.slice(idx + 2)})`; +} + +export function basenameSkill(dirName: string): string { + return basename(dirName); +} diff --git a/cursor-extension/src/extension.ts b/cursor-extension/src/extension.ts new file mode 100644 index 00000000..8058c983 --- /dev/null +++ b/cursor-extension/src/extension.ts @@ -0,0 +1,70 @@ +import * as vscode from "vscode"; +import { HealthPoller } from "./statusbar/poller"; +import { getStatusBarPresentation } from "./statusbar/indicator"; +import { registerHivemindCommands } from "./statusbar/commands"; +import { showStatusDetail } from "./statusbar/detail-view"; +import { DashboardPanel, registerDashboardWebview } from "./webview/DashboardPanel"; +import { runAutoSyncOnActivation } from "./bridge/auto-sync"; +import { logSafe } from "./utils/output"; +import type { StatusSnapshot } from "./types/health"; + +let statusBarItem: vscode.StatusBarItem | undefined; +let poller: HealthPoller | undefined; + +export function activate(context: vscode.ExtensionContext): void { + logSafe("Hivemind extension activating…"); + + poller = new HealthPoller(); + statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100); + statusBarItem.command = "hivemind.showStatus"; + statusBarItem.show(); + context.subscriptions.push(statusBarItem); + + const updateBar = (snap: StatusSnapshot): void => { + const pres = getStatusBarPresentation(snap.barState); + statusBarItem!.text = pres.text; + statusBarItem!.tooltip = snap.tooltip; + statusBarItem!.backgroundColor = new vscode.ThemeColor(pres.backgroundColor); + }; + + context.subscriptions.push( + poller.onUpdate((snap) => updateBar(snap)), + ...registerHivemindCommands( + () => poller!.pollOnce(workspaceRoot), + () => { + void poller!.pollOnce(workspaceRoot).then((snap) => showStatusDetail(snap)); + }, + ), + vscode.commands.registerCommand("hivemind.openDashboard", () => { + DashboardPanel.createOrShow(context.extensionUri, context); + }), + ); + + registerDashboardWebview(context); + + const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + void runAutoSyncOnActivation(workspaceRoot); + + poller.start(); + + if (!context.globalState.get("hivemind.onboardingPrompted")) { + void poller.pollOnce(workspaceRoot).then(async (snap) => { + if (snap.barState !== "healthy") { + const run = await vscode.window.showInformationMessage( + "Hivemind is not fully configured for Cursor yet.", + "Run onboarding", + "Later", + ); + if (run === "Run onboarding") { + await vscode.commands.executeCommand("hivemind.runOnboarding"); + } + } + await context.globalState.update("hivemind.onboardingPrompted", true); + }); + } +} + +export function deactivate(): void { + poller?.stop(); + statusBarItem?.dispose(); +} diff --git a/cursor-extension/src/graph/editor-sync.ts b/cursor-extension/src/graph/editor-sync.ts new file mode 100644 index 00000000..74f3bc60 --- /dev/null +++ b/cursor-extension/src/graph/editor-sync.ts @@ -0,0 +1,130 @@ +import * as vscode from "vscode"; +import type { GraphNode, GraphSnapshot } from "./types"; + +export interface ParsedLocation { + startLine: number; + endLine: number; +} + +/** Parse `L` or `L-` (1-indexed). */ +export function parseSourceLocation(loc: string): ParsedLocation { + const m = loc.match(/^L(\d+)(?:-(\d+))?/); + if (!m) return { startLine: 1, endLine: 1 }; + const startLine = parseInt(m[1]!, 10); + const endLine = m[2] ? parseInt(m[2], 10) : startLine; + return { startLine, endLine }; +} + +function locationSpan(loc: string): { start: number; end: number } { + const { startLine, endLine } = parseSourceLocation(loc); + return { start: startLine, end: Math.max(startLine, endLine) }; +} + +function rangeSize(loc: string): number { + const { start, end } = locationSpan(loc); + return end - start + 1; +} + +/** Find the best-matching node for a file path and 1-indexed cursor line. */ +export function findNodesAtPosition( + snapshot: GraphSnapshot, + relativeFile: string, + line: number, +): GraphNode[] { + const normalized = relativeFile.replace(/\\/g, "/").replace(/^\//, ""); + const candidates = snapshot.nodes.filter((n) => n.source_file === normalized); + const enclosing = candidates.filter((n) => { + const { start, end } = locationSpan(n.source_location); + return line >= start && line <= end; + }); + const pool = enclosing.length > 0 ? enclosing : candidates.filter((n) => parseSourceLocation(n.source_location).startLine === line); + return pool.sort((a, b) => { + const sizeDiff = rangeSize(a.source_location) - rangeSize(b.source_location); + if (sizeDiff !== 0) return sizeDiff; + return a.id.localeCompare(b.id); + }); +} + +function toWorkspaceUri(repoRoot: string, sourceFile: string): vscode.Uri { + return vscode.Uri.file(`${repoRoot.replace(/\/$/, "")}/${sourceFile}`); +} + +/** Open a graph node in the editor and reveal its declaration range. */ +export async function openNodeInEditor( + node: GraphNode, + repoRoot: string, +): Promise<{ ok: boolean; stale?: boolean; message?: string }> { + const uri = toWorkspaceUri(repoRoot, node.source_file); + const { startLine, endLine } = parseSourceLocation(node.source_location); + try { + const doc = await vscode.workspace.openTextDocument(uri); + const lineCount = doc.lineCount; + const line = Math.min(Math.max(1, startLine), lineCount) - 1; + const end = Math.min(Math.max(line, endLine - 1), lineCount - 1); + const range = new vscode.Range(line, 0, end, doc.lineAt(end).text.length); + const editor = await vscode.window.showTextDocument(doc, { preview: false }); + editor.selection = new vscode.Selection(range.start, range.end); + editor.revealRange(range, vscode.TextEditorRevealType.InCenter); + const stale = startLine > lineCount; + return { + ok: true, + stale, + message: stale ? "Graph location may be stale relative to the open file." : undefined, + }; + } catch { + return { ok: false, message: `Could not open ${node.source_file}` }; + } +} + +export interface EditorGraphSyncHandle { + dispose(): void; +} + +/** + * Highlight graph nodes that match the active editor cursor. + * `onHighlight` receives node ids (empty when none match). + */ +export function startEditorToGraphSync( + snapshot: GraphSnapshot, + repoRoot: string, + onHighlight: (nodeIds: string[]) => void, + debounceMs = 200, +): EditorGraphSyncHandle { + let timer: ReturnType | undefined; + + const syncActive = (): void => { + const editor = vscode.window.activeTextEditor; + if (!editor) { + onHighlight([]); + return; + } + const folder = vscode.workspace.getWorkspaceFolder(editor.document.uri); + const root = folder?.uri.fsPath ?? repoRoot; + const rel = vscode.workspace.asRelativePath(editor.document.uri, false).replace(/\\/g, "/"); + if (rel.startsWith("..")) { + onHighlight([]); + return; + } + const line = editor.selection.active.line + 1; + const matches = findNodesAtPosition(snapshot, rel, line); + onHighlight(matches.map((n) => n.id)); + }; + + const schedule = (): void => { + if (timer) clearTimeout(timer); + timer = setTimeout(syncActive, debounceMs); + }; + + const subs = [ + vscode.window.onDidChangeActiveTextEditor(schedule), + vscode.window.onDidChangeTextEditorSelection(schedule), + ]; + schedule(); + + return { + dispose(): void { + if (timer) clearTimeout(timer); + for (const s of subs) s.dispose(); + }, + }; +} diff --git a/cursor-extension/src/graph/impact-overlay.ts b/cursor-extension/src/graph/impact-overlay.ts new file mode 100644 index 00000000..53de7d9e --- /dev/null +++ b/cursor-extension/src/graph/impact-overlay.ts @@ -0,0 +1,136 @@ +import { execFileSync } from "node:child_process"; +import type { GraphEdge, GraphNode, GraphSnapshot } from "./types"; + +const IMPACT_CAP = 80; +const MAX_DEPTH = 25; + +const SOURCE_GLOBS = ["*.ts", "*.tsx", "*.js", "*.jsx", "*.mjs", "*.cjs", "*.py", "*.pyi", ":(exclude)*.d.ts"]; + +export interface ImpactNodeEntry { + id: string; + depth: number; + via?: { rel: string; from: string }; +} + +export interface ImpactOverlayResult { + changedFiles: string[]; + originNodeIds: string[]; + dependents: ImpactNodeEntry[]; + totalDependents: number; + capped: boolean; + caveat: string; +} + +function listUnstagedSourceFiles(cwd: string): string[] { + try { + const out = execFileSync("git", ["diff", "--name-only", "--", ...SOURCE_GLOBS], { + cwd, + encoding: "utf-8", + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + if (!out) return []; + return out.split(/\r?\n/).map((f) => f.replace(/\\/g, "/")).filter(Boolean); + } catch { + return []; + } +} + +function reverseBfsFromOrigins( + snap: GraphSnapshot, + originIds: string[], +): { dependents: ImpactNodeEntry[]; total: number; capped: boolean } { + const nodeIds = new Set(snap.nodes.map((n) => n.id)); + const incoming = new Map(); + for (const e of snap.links) { + if (!nodeIds.has(e.source)) continue; + const list = incoming.get(e.target); + if (list) list.push(e); + else incoming.set(e.target, [e]); + } + + const depthOf = new Map(); + const viaOf = new Map(); + let frontier = [...new Set(originIds)].filter((id) => nodeIds.has(id)); + for (const id of frontier) depthOf.set(id, 0); + + let depth = 0; + while (frontier.length > 0 && depth < MAX_DEPTH) { + depth++; + const next: string[] = []; + for (const id of frontier) { + const edges = (incoming.get(id) ?? []).slice().sort((a, b) => + a.source.localeCompare(b.source) || a.relation.localeCompare(b.relation)); + for (const e of edges) { + if (depthOf.has(e.source)) continue; + depthOf.set(e.source, depth); + viaOf.set(e.source, { rel: e.relation, from: id }); + next.push(e.source); + } + } + next.sort(); + frontier = next; + } + + const dependents: ImpactNodeEntry[] = []; + for (const [id, d] of depthOf.entries()) { + if (originIds.includes(id)) continue; + dependents.push({ id, depth: d, via: viaOf.get(id) }); + } + dependents.sort((a, b) => a.depth - b.depth || a.id.localeCompare(b.id)); + + const total = dependents.length; + const capped = total > IMPACT_CAP; + return { + dependents: dependents.slice(0, IMPACT_CAP), + total, + capped, + }; +} + +/** Compute git-diff-based impact visualization data for the graph canvas. */ +export function computeImpactOverlay(snapshot: GraphSnapshot, cwd: string): ImpactOverlayResult { + const changedFiles = listUnstagedSourceFiles(cwd); + const originNodeIds = snapshot.nodes + .filter((n) => changedFiles.includes(n.source_file)) + .map((n) => n.id) + .sort(); + + const caveat = + "Resolved graph edges only; real impact may be larger. Unstaged source changes mapped by file path."; + + if (changedFiles.length === 0) { + return { + changedFiles: [], + originNodeIds: [], + dependents: [], + totalDependents: 0, + capped: false, + caveat, + }; + } + + if (originNodeIds.length === 0) { + return { + changedFiles, + originNodeIds: [], + dependents: [], + totalDependents: 0, + capped: false, + caveat: `${caveat} Changed files have no matching graph nodes (new file or rebuild needed).`, + }; + } + + const { dependents, total, capped } = reverseBfsFromOrigins(snapshot, originNodeIds); + return { + changedFiles, + originNodeIds, + dependents, + totalDependents: total, + capped, + caveat, + }; +} + +export function nodesById(snapshot: GraphSnapshot): Map { + return new Map(snapshot.nodes.map((n) => [n.id, n])); +} diff --git a/cursor-extension/src/graph/snapshot-loader.ts b/cursor-extension/src/graph/snapshot-loader.ts new file mode 100644 index 00000000..366b2240 --- /dev/null +++ b/cursor-extension/src/graph/snapshot-loader.ts @@ -0,0 +1,90 @@ +import type { DashboardDataEnvelope } from "../webview/data-bridge"; +import type { GraphEdge, GraphNode, GraphSnapshot } from "./types"; + +function isObject(v: unknown): v is Record { + return v !== null && typeof v === "object" && !Array.isArray(v); +} + +function isGraphSnapshotLike(raw: unknown): raw is GraphSnapshot { + if (!isObject(raw)) return false; + if (!Array.isArray(raw.nodes) || !Array.isArray(raw.links)) return false; + return raw.directed === true && raw.multigraph === true && isObject(raw.graph); +} + +/** Load a typed graph snapshot from a dashboard data envelope. */ +export function loadGraphSnapshotFromEnvelope(envelope: DashboardDataEnvelope): GraphSnapshot | null { + if (!envelope.graph?.snapshot) return null; + return parseGraphSnapshot(envelope.graph.snapshot); +} + +/** Parse and validate a raw snapshot payload from disk or the webview bridge. */ +export function parseGraphSnapshot(raw: unknown): GraphSnapshot | null { + if (!isGraphSnapshotLike(raw)) return null; + const nodes: GraphNode[] = []; + for (const n of raw.nodes) { + if (!isObject(n)) continue; + const id = typeof n.id === "string" ? n.id : null; + const label = typeof n.label === "string" ? n.label : null; + const kind = typeof n.kind === "string" ? n.kind : null; + const source_file = typeof n.source_file === "string" ? n.source_file : null; + const source_location = typeof n.source_location === "string" ? n.source_location : null; + const language = typeof n.language === "string" ? n.language : null; + if (!id || !label || !kind || !source_file || !source_location || !language) continue; + nodes.push({ + id, + label, + kind: kind as GraphNode["kind"], + source_file, + source_location, + language: language as GraphNode["language"], + exported: Boolean(n.exported), + signature: typeof n.signature === "string" ? n.signature : undefined, + doc: typeof n.doc === "string" ? n.doc : undefined, + fan_in: typeof n.fan_in === "number" ? n.fan_in : undefined, + fan_out: typeof n.fan_out === "number" ? n.fan_out : undefined, + is_entrypoint: typeof n.is_entrypoint === "boolean" ? n.is_entrypoint : undefined, + }); + } + if (nodes.length === 0 && raw.nodes.length > 0) return null; + + const links: GraphEdge[] = []; + for (const l of raw.links) { + if (!isObject(l)) continue; + const source = typeof l.source === "string" ? l.source : null; + const target = typeof l.target === "string" ? l.target : null; + const relation = typeof l.relation === "string" ? l.relation : null; + if (!source || !target || !relation) continue; + links.push({ + source, + target, + relation: relation as GraphEdge["relation"], + confidence: typeof l.confidence === "string" ? (l.confidence as GraphEdge["confidence"]) : undefined, + ord: typeof l.ord === "number" ? l.ord : undefined, + }); + } + + const graph = raw.graph; + return { + directed: true, + multigraph: true, + graph: { + schema_version: 1, + generator: "hivemind-graph", + commit_sha: typeof graph.commit_sha === "string" || graph.commit_sha === null ? graph.commit_sha : null, + repo_key: typeof graph.repo_key === "string" ? graph.repo_key : "", + }, + observation: isObject(raw.observation) + ? (raw.observation as GraphSnapshot["observation"]) + : { + ts: new Date().toISOString(), + branch: null, + worktree_path: "", + repo_project: "", + generator_version: "unknown", + source_files_extracted: 0, + source_files_skipped: 0, + }, + nodes, + links, + }; +} diff --git a/cursor-extension/src/graph/types.ts b/cursor-extension/src/graph/types.ts new file mode 100644 index 00000000..8a0c29e0 --- /dev/null +++ b/cursor-extension/src/graph/types.ts @@ -0,0 +1,50 @@ +export type NodeKind = + | "function" + | "class" + | "method" + | "interface" + | "type_alias" + | "enum" + | "const" + | "module"; + +export type EdgeRelation = "imports" | "calls" | "extends" | "implements" | "method_of"; + +export type EdgeConfidence = "EXTRACTED" | "INFERRED" | "AMBIGUOUS"; + +export interface GraphNode { + id: string; + label: string; + kind: NodeKind; + source_file: string; + source_location: string; + language: string; + exported: boolean; + signature?: string; + doc?: string; + fan_in?: number; + fan_out?: number; + is_entrypoint?: boolean; +} + +export interface GraphEdge { + source: string; + target: string; + relation: EdgeRelation; + confidence?: EdgeConfidence; + ord?: number; +} + +export interface GraphSnapshot { + directed: true; + multigraph: true; + graph: { + schema_version: number; + generator: string; + commit_sha: string | null; + repo_key: string; + }; + observation?: Record; + nodes: GraphNode[]; + links: GraphEdge[]; +} diff --git a/cursor-extension/src/health/checker.ts b/cursor-extension/src/health/checker.ts new file mode 100644 index 00000000..3d01d4a9 --- /dev/null +++ b/cursor-extension/src/health/checker.ts @@ -0,0 +1,294 @@ +import { execFileSync } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import type { HealthDimension, HealthResult } from "../types/health"; +import { + cursorBundleDir, + cursorHooksPath, + hivemindCursorBundleSrc, +} from "../utils/paths"; +import { readJson } from "../utils/fs-json"; + +const HIVEMIND_MARKER_KEY = "_hivemindManaged"; +const DOCS_URL = "https://github.com/thenotoriousllama/hivemind#quick-start"; + +interface CursorHookEntry { + type: "command" | "prompt"; + command?: string; + timeout?: number; + matcher?: string | Record; +} + +function resolveCliBin(cli: string, fallbacks: string[] = []): string | null { + const isWin = process.platform === "win32"; + try { + const out = execFileSync(isWin ? "where" : "which", [cli], { encoding: "utf-8" }); + const match = out.split(/\r?\n/).map((l) => l.trim()).find(Boolean); + if (match) return match; + } catch { + /* not on PATH */ + } + for (const fb of fallbacks) { + if (existsSync(fb)) return fb; + } + return null; +} + +function cursorAgentFallbacks(): string[] { + const home = homedir(); + return [ + "/usr/local/bin/cursor-agent", + "/usr/bin/cursor-agent", + join(home, ".npm-global", "bin", "cursor-agent"), + join(home, ".local", "bin", "cursor-agent"), + "/opt/homebrew/bin/cursor-agent", + ]; +} + +function probeVersion(bin: string): string | undefined { + try { + const out = execFileSync(bin, ["--version"], { encoding: "utf-8", timeout: 5000 }).trim(); + return out.split(/\r?\n/)[0] || undefined; + } catch { + return undefined; + } +} + +export function isHivemindEntry(entry: unknown): boolean { + if (!entry || typeof entry !== "object") return false; + const cmd = (entry as { command?: string }).command; + if (typeof cmd !== "string") return false; + return cmd.replace(/\\/g, "/").includes("/.cursor/hivemind/bundle/"); +} + +function buildHookCmd(bundleFile: string, pluginDir: string, timeout: number): CursorHookEntry { + return { + type: "command", + command: `node "${join(pluginDir, "bundle", bundleFile)}"`, + timeout, + }; +} + +function buildHookCmdShellMatcher(bundleFile: string, pluginDir: string, timeout: number): CursorHookEntry { + return { + type: "command", + command: `node "${join(pluginDir, "bundle", bundleFile)}"`, + timeout, + matcher: "Shell", + }; +} + +export function buildHookConfig(pluginDir: string, version: string): Record { + return { + sessionStart: [buildHookCmd("session-start.js", pluginDir, 30)], + beforeSubmitPrompt: [buildHookCmd("capture.js", pluginDir, 10)], + preToolUse: [buildHookCmdShellMatcher("pre-tool-use.js", pluginDir, 30)], + postToolUse: [buildHookCmd("capture.js", pluginDir, 15)], + afterAgentResponse: [buildHookCmd("capture.js", pluginDir, 15)], + stop: [buildHookCmd("capture.js", pluginDir, 15), buildHookCmd("graph-on-stop.js", pluginDir, 30)], + sessionEnd: [buildHookCmd("session-end.js", pluginDir, 30), buildHookCmd("graph-on-stop.js", pluginDir, 30)], + }; +} + +function readExtensionVersion(): string { + try { + const pkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8")); + return typeof pkg.version === "string" ? pkg.version : "0.0.0"; + } catch { + return "0.0.0"; + } +} + +function readBundleVersion(): string | undefined { + const stamp = join(cursorBundleDir(), ".version"); + if (!existsSync(stamp)) return undefined; + try { + return readFileSync(stamp, "utf-8").trim() || undefined; + } catch { + return undefined; + } +} + +function checkHivemindCli(): HealthDimension { + const bin = resolveCliBin("hivemind"); + if (!bin) { + return { + id: "d1", + label: "Hivemind CLI", + status: "missing", + message: "Hivemind CLI not found on PATH.", + remediation: "Install Hivemind CLI to enable shared memory.", + installCommand: "npm install -g @deeplake/hivemind", + docsUrl: DOCS_URL, + }; + } + const version = probeVersion(bin); + return { + id: "d1", + label: "Hivemind CLI", + status: "ok", + message: version ? `Found ${bin} (${version})` : `Found ${bin}`, + }; +} + +function checkCursorAgentCli(): HealthDimension { + const bin = resolveCliBin("cursor-agent", cursorAgentFallbacks()); + if (!bin) { + return { + id: "d2", + label: "cursor-agent CLI", + status: "missing", + message: "cursor-agent not found. Session summaries are disabled until it is installed.", + remediation: "Install cursor-agent and ensure it is on PATH.", + docsUrl: "https://cursor.com/docs/agent/cli", + }; + } + const version = probeVersion(bin); + return { + id: "d2", + label: "cursor-agent CLI", + status: "ok", + message: version ? `Found ${bin} (${version})` : `Found ${bin}`, + }; +} + +function checkCursorAgentLogin(): HealthDimension { + const bin = resolveCliBin("cursor-agent", cursorAgentFallbacks()); + if (!bin) { + return { + id: "d3", + label: "cursor-agent login", + status: "missing", + message: "cursor-agent not installed; cannot verify login.", + }; + } + try { + execFileSync(bin, ["status"], { encoding: "utf-8", timeout: 8000 }); + return { + id: "d3", + label: "cursor-agent login", + status: "ok", + message: "cursor-agent is logged in.", + }; + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + const loggedOut = + msg.includes("not logged") || + msg.includes("Not logged") || + msg.includes("login") || + msg.includes("401") || + msg.includes("ENOENT"); + return { + id: "d3", + label: "cursor-agent login", + status: loggedOut ? "logged_out" : "error", + message: loggedOut + ? "cursor-agent is installed but logged out. Summaries will silently fail until you log in." + : `Could not verify cursor-agent login: ${msg}`, + remediation: "Run `cursor-agent login` in a terminal or use Hivemind onboarding.", + }; + } +} + +function checkHooksWired(bundleVersion: string | undefined): { + dimension: HealthDimension; + wiredVersion?: string; +} { + const hooksPath = cursorHooksPath(); + const existing = readJson<{ hooks?: Record; [key: string]: unknown }>(hooksPath); + if (!existing?.hooks) { + return { + dimension: { + id: "d4", + label: "Hooks wired", + status: "not_wired", + message: "Hivemind hooks are not wired in ~/.cursor/hooks.json.", + remediation: "Use Wire / Refresh Hooks to install lifecycle hooks.", + }, + }; + } + + const events = ["sessionStart", "beforeSubmitPrompt", "preToolUse", "postToolUse", "afterAgentResponse", "stop", "sessionEnd"]; + const missing: string[] = []; + for (const ev of events) { + const entries = existing.hooks[ev]; + if (!Array.isArray(entries) || !entries.some(isHivemindEntry)) { + missing.push(ev); + } + } + + const marker = existing[HIVEMIND_MARKER_KEY] as { version?: string } | undefined; + const wiredVersion = marker?.version; + + if (missing.length > 0) { + return { + dimension: { + id: "d4", + label: "Hooks wired", + status: "not_wired", + message: `Missing Hivemind hooks for: ${missing.join(", ")}`, + remediation: "Use Wire / Refresh Hooks to complete wiring.", + }, + wiredVersion, + }; + } + + if (bundleVersion && wiredVersion && wiredVersion !== bundleVersion) { + return { + dimension: { + id: "d4", + label: "Hooks wired", + status: "stale", + message: `Hooks wired at v${wiredVersion}; current bundle is v${bundleVersion}.`, + remediation: "Refresh hooks to update to the current bundle version.", + }, + wiredVersion, + }; + } + + return { + dimension: { + id: "d4", + label: "Hooks wired", + status: "ok", + message: wiredVersion ? `All seven hooks wired (v${wiredVersion}).` : "All seven hooks wired.", + }, + wiredVersion, + }; +} + +export async function runHealthCheck(): Promise { + const bundlePresent = existsSync(cursorBundleDir()) && existsSync(join(cursorBundleDir(), "capture.js")); + const bundleVersion = readBundleVersion() ?? readExtensionVersion(); + const srcBundle = hivemindCursorBundleSrc(); + const srcPresent = existsSync(join(srcBundle, "capture.js")); + + const d1 = checkHivemindCli(); + const d2 = checkCursorAgentCli(); + const d3 = checkCursorAgentLogin(); + const { dimension: d4, wiredVersion } = checkHooksWired(bundlePresent ? bundleVersion : undefined); + + if (!bundlePresent && !srcPresent) { + d4.status = "error"; + d4.message = "Hook bundle missing at ~/.cursor/hivemind/bundle/. Run hivemind cursor install or Wire Hooks after building."; + } + + const dimensions = [d1, d2, d3, d4]; + const summariesDisabled = d2.status !== "ok" || d3.status !== "ok"; + const allHealthy = dimensions.every((d) => d.status === "ok") && bundlePresent; + + return { + checkedAt: new Date().toISOString(), + dimensions, + bundlePresent, + bundleVersion, + wiredVersion, + allHealthy, + summariesDisabled, + }; +} + +export function getHivemindInstallCommand(): string { + return "npm install -g @deeplake/hivemind"; +} diff --git a/cursor-extension/src/health/index.ts b/cursor-extension/src/health/index.ts new file mode 100644 index 00000000..24a68e58 --- /dev/null +++ b/cursor-extension/src/health/index.ts @@ -0,0 +1,3 @@ +export { runHealthCheck, getHivemindInstallCommand } from "./checker"; +export { autoWireHooks, unwireHooks } from "./wirings"; +export type { WireResult } from "./wirings"; diff --git a/cursor-extension/src/health/wirings.ts b/cursor-extension/src/health/wirings.ts new file mode 100644 index 00000000..376c4c6b --- /dev/null +++ b/cursor-extension/src/health/wirings.ts @@ -0,0 +1,122 @@ +import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { buildHookConfig, isHivemindEntry } from "./checker"; +import { + cursorBundleDir, + cursorHooksPath, + cursorPluginDir, + hivemindCursorBundleSrc, +} from "../utils/paths"; +import { readJson, writeJsonIfChanged } from "../utils/fs-json"; + +const HIVEMIND_MARKER_KEY = "_hivemindManaged"; + +export interface WireResult { + ok: boolean; + changed: boolean; + message: string; + reloadRequired: boolean; +} + +function readExtensionVersion(): string { + try { + const pkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8")); + return typeof pkg.version === "string" ? pkg.version : "0.0.0"; + } catch { + return "0.0.0"; + } +} + +function mergeHooks(existing: Record | null, pluginDir: string, version: string): Record { + const root = (existing ?? { version: 1, hooks: {} }) as { + version?: number; + hooks?: Record; + }; + if (!root.version) root.version = 1; + if (!root.hooks) root.hooks = {}; + const ours = buildHookConfig(pluginDir, version); + for (const [event, entries] of Object.entries(ours)) { + const prior = Array.isArray(root.hooks[event]) ? root.hooks[event] : []; + const stripped = prior.filter((e) => !isHivemindEntry(e)); + root.hooks[event] = [...stripped, ...entries]; + } + (root as Record)[HIVEMIND_MARKER_KEY] = { version }; + return root as unknown as Record; +} + +export function stripHooksFromConfig(existing: Record | null): Record | null { + if (!existing) return null; + const root = existing as { hooks?: Record }; + if (root.hooks) { + for (const event of Object.keys(root.hooks)) { + root.hooks[event] = (root.hooks[event] ?? []).filter((e) => !isHivemindEntry(e)); + if (root.hooks[event].length === 0) delete root.hooks[event]; + } + if (Object.keys(root.hooks).length === 0) delete root.hooks; + } + delete (existing as Record)[HIVEMIND_MARKER_KEY]; + return existing; +} + +function provisionBundle(): { ok: boolean; message: string } { + const src = hivemindCursorBundleSrc(); + if (!existsSync(join(src, "capture.js"))) { + return { + ok: false, + message: `Cursor bundle missing at ${src}. Run 'npm run build' in the hivemind repo first.`, + }; + } + mkdirSync(cursorPluginDir(), { recursive: true }); + cpSync(src, cursorBundleDir(), { recursive: true, force: true }); + const version = readExtensionVersion(); + writeFileSync(join(cursorPluginDir(), ".version"), version + "\n"); + return { ok: true, message: "Bundle provisioned." }; +} + +export async function autoWireHooks(): Promise { + const provision = provisionBundle(); + if (!provision.ok) { + return { ok: false, changed: false, message: provision.message, reloadRequired: false }; + } + + const version = readExtensionVersion(); + const existing = readJson>(cursorHooksPath()); + const merged = mergeHooks(existing, cursorPluginDir(), version); + const changed = writeJsonIfChanged(cursorHooksPath(), merged); + + return { + ok: true, + changed, + message: changed + ? "Hooks wired. Reload Cursor to activate the new hooks." + : "Hooks already up to date; hooks.json was not rewritten.", + reloadRequired: changed, + }; +} + +export async function unwireHooks(): Promise { + const existing = readJson>(cursorHooksPath()); + if (!existing) { + return { ok: true, changed: false, message: "No hooks.json to clean.", reloadRequired: false }; + } + const stripped = stripHooksFromConfig(existing); + const meaningfulKeys = stripped + ? Object.keys(stripped).filter((k) => k !== "version").length + : 0; + + if (!stripped || meaningfulKeys === 0) { + if (existsSync(cursorHooksPath())) { + const { unlinkSync } = await import("node:fs"); + unlinkSync(cursorHooksPath()); + } + return { ok: true, changed: true, message: "Hivemind hooks removed.", reloadRequired: true }; + } + + const changed = writeJsonIfChanged(cursorHooksPath(), stripped); + return { + ok: true, + changed, + message: "Hivemind hooks stripped; foreign hooks preserved.", + reloadRequired: changed, + }; +} diff --git a/cursor-extension/src/statusbar/commands.ts b/cursor-extension/src/statusbar/commands.ts new file mode 100644 index 00000000..b87dd705 --- /dev/null +++ b/cursor-extension/src/statusbar/commands.ts @@ -0,0 +1,61 @@ +import * as vscode from "vscode"; +import type { StatusSnapshot } from "../types/health"; +import { autoWireHooks } from "../health"; +import { promptLoginMethod, logout } from "../auth"; +import { getOutputChannel } from "../utils/output"; +import { wikiWorkerLogPath } from "../utils/paths"; +import { existsSync } from "node:fs"; +import { readFileSync } from "node:fs"; + +export async function runOnboarding(poll: () => Promise): Promise { + const wire = await autoWireHooks(); + if (!wire.ok) { + await vscode.window.showErrorMessage(wire.message); + } else if (wire.reloadRequired) { + await vscode.window.showInformationMessage(wire.message, "Reload Window").then((c) => { + if (c === "Reload Window") void vscode.commands.executeCommand("workbench.action.reloadWindow"); + }); + } + await promptLoginMethod(); + await poll(); +} + +export async function wireHooksCommand(): Promise { + const result = await autoWireHooks(); + if (result.ok) { + const actions = result.reloadRequired ? ["Reload Window"] : []; + await vscode.window.showInformationMessage(result.message, ...actions).then((c) => { + if (c === "Reload Window") void vscode.commands.executeCommand("workbench.action.reloadWindow"); + }); + } else { + await vscode.window.showErrorMessage(result.message); + } +} + +export function openLogsCommand(): void { + const ch = getOutputChannel(); + ch.show(true); + if (existsSync(wikiWorkerLogPath())) { + try { + const tail = readFileSync(wikiWorkerLogPath(), "utf-8").split(/\r?\n/).slice(-40).join("\n"); + ch.appendLine("--- wiki-worker.log (tail) ---"); + ch.appendLine(tail); + } catch { + ch.appendLine("(could not read wiki-worker.log)"); + } + } +} + +export function registerHivemindCommands( + poll: () => Promise, + showDetail: () => void, +): vscode.Disposable[] { + return [ + vscode.commands.registerCommand("hivemind.runOnboarding", () => runOnboarding(poll)), + vscode.commands.registerCommand("hivemind.login", () => promptLoginMethod().then(() => poll())), + vscode.commands.registerCommand("hivemind.logout", () => logout().then(() => poll())), + vscode.commands.registerCommand("hivemind.showStatus", showDetail), + vscode.commands.registerCommand("hivemind.wireHooks", wireHooksCommand), + vscode.commands.registerCommand("hivemind.openLogs", openLogsCommand), + ]; +} diff --git a/cursor-extension/src/statusbar/detail-view.ts b/cursor-extension/src/statusbar/detail-view.ts new file mode 100644 index 00000000..59bffe18 --- /dev/null +++ b/cursor-extension/src/statusbar/detail-view.ts @@ -0,0 +1,62 @@ +import * as vscode from "vscode"; +import type { StatusSnapshot } from "../types/health"; +import { formatIdentity } from "../auth/detector"; +import { promptLoginMethod } from "../auth"; +import { autoWireHooks, getHivemindInstallCommand } from "../health"; +import { wireHooksCommand } from "./commands"; + +export async function showStatusDetail(snapshot: StatusSnapshot): Promise { + const items: vscode.QuickPickItem[] = snapshot.health.dimensions.map((d) => ({ + label: `${d.status === "ok" ? "$(check)" : "$(warning)"} ${d.label}`, + description: d.message, + detail: d.remediation, + })); + + items.push({ + label: "$(key) Hivemind identity", + description: formatIdentity(snapshot.auth), + }); + + if (snapshot.auth.cursorAgentLoggedIn === false) { + items.push({ + label: "$(warning) cursor-agent login", + description: snapshot.auth.cursorAgentMessage ?? "Log in to cursor-agent for summaries", + }); + } + + items.push({ label: "", kind: vscode.QuickPickItemKind.Separator, description: "Actions" }); + items.push({ label: "$(sign-in) Log in to Hivemind", description: "Browser or API key" }); + items.push({ label: "$(plug) Wire / refresh hooks", description: "Merge Hivemind lifecycle hooks" }); + items.push({ label: "$(rocket) Run full onboarding", description: "Wire hooks + log in" }); + + const d1 = snapshot.health.dimensions.find((d) => d.id === "d1"); + if (d1?.installCommand) { + items.push({ + label: "$(copy) Copy Hivemind install command", + description: d1.installCommand, + }); + } + + const pick = await vscode.window.showQuickPick(items, { + title: "Hivemind Status", + matchOnDescription: true, + }); + if (!pick) return; + + if (pick.label.includes("Log in")) { + await promptLoginMethod(); + return; + } + if (pick.label.includes("Wire")) { + await wireHooksCommand(); + return; + } + if (pick.label.includes("onboarding")) { + await vscode.commands.executeCommand("hivemind.runOnboarding"); + return; + } + if (pick.label.includes("Copy Hivemind install")) { + await vscode.env.clipboard.writeText(getHivemindInstallCommand()); + await vscode.window.showInformationMessage("Install command copied to clipboard."); + } +} diff --git a/cursor-extension/src/statusbar/indicator.ts b/cursor-extension/src/statusbar/indicator.ts new file mode 100644 index 00000000..2020b1cd --- /dev/null +++ b/cursor-extension/src/statusbar/indicator.ts @@ -0,0 +1,73 @@ +import type { AuthState, HealthResult, SkillSyncState, StatusBarState, StatusSnapshot } from "../types/health"; +import { formatIdentity } from "../auth/detector"; + +export function composeStatusBarState(health: HealthResult, auth: AuthState, skillSync?: SkillSyncState): StatusBarState { + if (auth.state === "unknown_offline") return "unknown"; + if (auth.state === "logged_out") return "logged_out"; + + const d1 = health.dimensions.find((d) => d.id === "d1"); + const d2 = health.dimensions.find((d) => d.id === "d2"); + const d3 = health.dimensions.find((d) => d.id === "d3"); + const d4 = health.dimensions.find((d) => d.id === "d4"); + + const hooksMissing = d4?.status === "not_wired" || d4?.status === "stale" || d4?.status === "error"; + const cliMissing = d1?.status === "missing" || d2?.status === "missing"; + + if (cliMissing || hooksMissing) return "not_configured"; + + const summaryImpaired = d2?.status !== "ok" || d3?.status === "logged_out"; + if (summaryImpaired) return "degraded"; + + // Skill-sync failures degrade the status: team skills are not reaching the Cursor agent. + if (skillSync && skillSync.erroredCount > 0) return "degraded"; + + if (health.allHealthy && auth.state === "logged_in") return "healthy"; + if (auth.state === "logged_in" && !health.allHealthy) return "degraded"; + return "not_configured"; +} + +const STATE_LABELS: Record = { + healthy: "$(check) Hivemind", + degraded: "$(warning) Hivemind degraded", + not_configured: "$(gear) Hivemind setup", + logged_out: "$(sign-in) Hivemind logged out", + unknown: "$(question) Hivemind unknown", +}; + +const STATE_COLORS: Record = { + healthy: "statusBarItem.prominentBackground", + degraded: "statusBarItem.warningBackground", + not_configured: "statusBarItem.errorBackground", + logged_out: "statusBarItem.errorBackground", + unknown: "statusBarItem.warningBackground", +}; + +export function buildTooltip(health: HealthResult, auth: AuthState, skillSync?: SkillSyncState): string { + const lines = health.dimensions.map((d) => `${d.label}: ${d.message}`); + lines.push(`Hivemind login: ${formatIdentity(auth)}`); + if (auth.cursorAgentLoggedIn === false && auth.cursorAgentMessage) { + lines.push(`cursor-agent: ${auth.cursorAgentMessage}`); + } + if (skillSync && skillSync.erroredCount > 0) { + lines.push(`Skill sync: ${skillSync.erroredCount} skill(s) not reaching Cursor agent`); + } + return lines.join("\n"); +} + +export function buildSnapshot(health: HealthResult, auth: AuthState, skillSync?: SkillSyncState): StatusSnapshot { + const barState = composeStatusBarState(health, auth, skillSync); + return { + barState, + health, + auth, + tooltip: buildTooltip(health, auth, skillSync), + skillSync, + }; +} + +export function getStatusBarPresentation(state: StatusBarState): { text: string; backgroundColor: string } { + return { + text: STATE_LABELS[state], + backgroundColor: STATE_COLORS[state], + }; +} diff --git a/cursor-extension/src/statusbar/poller.ts b/cursor-extension/src/statusbar/poller.ts new file mode 100644 index 00000000..1f8d7289 --- /dev/null +++ b/cursor-extension/src/statusbar/poller.ts @@ -0,0 +1,52 @@ +import * as vscode from "vscode"; +import type { StatusSnapshot } from "../types/health"; +import { runHealthCheck } from "../health"; +import { detectAuthState } from "../auth"; +import { syncSkillsToCursor } from "../bridge/skill-sync"; +import { buildSnapshot } from "./indicator"; + +const DEFAULT_INTERVAL_MS = 60_000; + +export class HealthPoller { + private timer: NodeJS.Timeout | undefined; + private listeners = new Set<(snap: StatusSnapshot) => void>(); + private lastSnapshot: StatusSnapshot | undefined; + + onUpdate(listener: (snap: StatusSnapshot) => void): vscode.Disposable { + this.listeners.add(listener); + if (this.lastSnapshot) listener(this.lastSnapshot); + return new vscode.Disposable(() => this.listeners.delete(listener)); + } + + start(intervalMs = DEFAULT_INTERVAL_MS): void { + this.stop(); + void this.pollOnce(); + this.timer = setInterval(() => { + if (vscode.window.state.focused !== false) { + void this.pollOnce(); + } + }, intervalMs); + } + + stop(): void { + if (this.timer) { + clearInterval(this.timer); + this.timer = undefined; + } + } + + async pollOnce(projectRoot?: string): Promise { + const health = await runHealthCheck(); + const auth = await detectAuthState(); + let skillSync; + try { + skillSync = syncSkillsToCursor(projectRoot); + } catch { + /* best-effort; never block the poll */ + } + const snap = buildSnapshot(health, auth, skillSync); + this.lastSnapshot = snap; + for (const listener of this.listeners) listener(snap); + return snap; + } +} diff --git a/cursor-extension/src/types/health.ts b/cursor-extension/src/types/health.ts new file mode 100644 index 00000000..152dc2f8 --- /dev/null +++ b/cursor-extension/src/types/health.ts @@ -0,0 +1,62 @@ +export type HealthDimensionStatus = "ok" | "missing" | "logged_out" | "stale" | "not_wired" | "error"; + +export interface HealthDimension { + id: "d1" | "d2" | "d3" | "d4"; + label: string; + status: HealthDimensionStatus; + message: string; + remediation?: string; + installCommand?: string; + docsUrl?: string; +} + +export interface HealthResult { + checkedAt: string; + dimensions: HealthDimension[]; + bundlePresent: boolean; + bundleVersion?: string; + wiredVersion?: string; + allHealthy: boolean; + summariesDisabled: boolean; +} + +export type AuthLoginState = "logged_in" | "logged_out" | "unknown_offline"; + +export interface AuthState { + state: AuthLoginState; + identity?: string; + orgName?: string; + workspaceId?: string; + cursorAgentLoggedIn?: boolean; + cursorAgentMessage?: string; +} + +export interface SkillSyncResult { + skillName: string; + status: "synced" | "skipped" | "errored"; + path?: string; + reason?: string; +} + +export interface SkillSyncState { + lastSyncAt?: string; + results: SkillSyncResult[]; + syncedCount: number; + skippedCount: number; + erroredCount: number; +} + +export type StatusBarState = + | "healthy" + | "degraded" + | "not_configured" + | "logged_out" + | "unknown"; + +export interface StatusSnapshot { + barState: StatusBarState; + health: HealthResult; + auth: AuthState; + tooltip: string; + skillSync?: SkillSyncState; +} diff --git a/cursor-extension/src/utils/fs-json.ts b/cursor-extension/src/utils/fs-json.ts new file mode 100644 index 00000000..d9b006c1 --- /dev/null +++ b/cursor-extension/src/utils/fs-json.ts @@ -0,0 +1,29 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname } from "node:path"; + +export function readJson(path: string): T | null { + if (!existsSync(path)) return null; + try { + return JSON.parse(readFileSync(path, "utf-8")) as T; + } catch { + return null; + } +} + +export function writeJson(path: string, obj: unknown): void { + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, JSON.stringify(obj, null, 2) + "\n"); +} + +export function writeJsonIfChanged(path: string, obj: unknown): boolean { + const next = JSON.stringify(obj, null, 2) + "\n"; + if (existsSync(path)) { + try { + if (readFileSync(path, "utf-8") === next) return false; + } catch { + /* fall through */ + } + } + writeJson(path, obj); + return true; +} diff --git a/cursor-extension/src/utils/output.ts b/cursor-extension/src/utils/output.ts new file mode 100644 index 00000000..0c15ff3f --- /dev/null +++ b/cursor-extension/src/utils/output.ts @@ -0,0 +1,20 @@ +import * as vscode from "vscode"; + +let channel: vscode.OutputChannel | undefined; + +export function getOutputChannel(): vscode.OutputChannel { + if (!channel) { + channel = vscode.window.createOutputChannel("Hivemind"); + } + return channel; +} + +/** Safe log: never pass secrets through this helper. */ +export function logSafe(message: string): void { + getOutputChannel().appendLine(message); +} + +export function logError(message: string, err?: unknown): void { + const detail = err instanceof Error ? err.message : err ? String(err) : ""; + logSafe(detail ? `${message}: ${detail}` : message); +} diff --git a/cursor-extension/src/utils/paths.ts b/cursor-extension/src/utils/paths.ts new file mode 100644 index 00000000..b42ca1bc --- /dev/null +++ b/cursor-extension/src/utils/paths.ts @@ -0,0 +1,48 @@ +import { homedir } from "node:os"; +import { join } from "node:path"; + +export const HOME = homedir(); + +export function cursorHome(): string { + return join(HOME, ".cursor"); +} + +export function cursorHooksPath(): string { + return join(cursorHome(), "hooks.json"); +} + +export function cursorPluginDir(): string { + return join(cursorHome(), "hivemind"); +} + +export function cursorBundleDir(): string { + return join(cursorPluginDir(), "bundle"); +} + +export function deeplakeConfigDir(): string { + return join(HOME, ".deeplake"); +} + +export function credentialsPath(): string { + return join(deeplakeConfigDir(), "credentials.json"); +} + +export function wikiWorkerLogPath(): string { + return join(deeplakeConfigDir(), "wiki-worker.log"); +} + +export function hivemindGraphsHome(): string { + return process.env.HIVEMIND_GRAPHS_HOME ?? join(HOME, ".hivemind", "graphs"); +} + +export function monorepoRoot(): string { + return join(__dirname, "..", "..", ".."); +} + +export function hivemindCursorBundleSrc(): string { + return join(monorepoRoot(), "harnesses", "cursor", "bundle"); +} + +export function workspaceRoot(fallback?: string): string { + return fallback ?? process.cwd(); +} diff --git a/cursor-extension/src/webview/DashboardPanel.ts b/cursor-extension/src/webview/DashboardPanel.ts new file mode 100644 index 00000000..5e17c7b4 --- /dev/null +++ b/cursor-extension/src/webview/DashboardPanel.ts @@ -0,0 +1,419 @@ +import { existsSync, readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import * as vscode from "vscode"; +import { backfillCursorLinks, listLocalSkillsForPromoter, skillDirLabel, syncSkillsToCursor } from "../bridge/skill-sync"; +import { detectAuthState, formatIdentity, loadStoredCredentials } from "../auth"; +import { runHealthCheck } from "../health/checker"; +import { computeImpactOverlay } from "../graph/impact-overlay"; +import { openNodeInEditor, startEditorToGraphSync, type EditorGraphSyncHandle } from "../graph/editor-sync"; +import { loadGraphSnapshotFromEnvelope, parseGraphSnapshot } from "../graph/snapshot-loader"; +import type { GraphSnapshot } from "../graph/types"; +import { logError } from "../utils/output"; +import { getDashboardHtml } from "./html/dashboard-shell"; +import { loadDashboardData, loadRecentSessions, runHivemindCli } from "./data-bridge"; + +type DashboardPane = "kpi" | "settings" | "sessions" | "graph" | "rules" | "skills"; + +const NEXT_STEPS_SECTION_RE = /^##\s+Next Steps\s*$/im; + +interface WebviewInboundMessage { + type: string; + pane?: DashboardPane; + sessionId?: string; + text?: string; + ruleId?: string; + nodeId?: string; + dirName?: string; + orgName?: string; +} + +interface ParsedRule { + id: string; + status: string; + version: number; + author: string; + text: string; +} + +function workspaceRoot(): string { + return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? process.cwd(); +} + +const SESSION_ID_RE = /^[a-zA-Z0-9_-]{1,128}$/; + +function readSessionSummary(sessionId: string): string | null { + if (!SESSION_ID_RE.test(sessionId)) return null; + const creds = loadStoredCredentials(); + const user = creds?.userName ?? "unknown"; + if (user.includes("/") || user.includes("\\") || user.includes("..")) return null; + const path = join(homedir(), ".deeplake", "memory", "summaries", user, `${sessionId}.md`); + if (!existsSync(path)) return null; + try { + return readFileSync(path, "utf-8"); + } catch { + return null; + } +} + +function extractNextSteps(summary: string): string[] { + const match = NEXT_STEPS_SECTION_RE.exec(summary); + if (!match) return []; + const after = summary.slice(match.index + match[0].length); + const nextSection = after.search(/^##\s/m); + const block = nextSection >= 0 ? after.slice(0, nextSection) : after; + return block + .split(/\r?\n/) + .map((l) => l.replace(/^[-*]\s*/, "").trim()) + .filter((l) => l.length > 0 && !l.startsWith("#")); +} + +function parseRulesList(stdout: string): ParsedRule[] { + const rules: ParsedRule[] = []; + for (const line of stdout.split(/\r?\n/)) { + const m = line.match(/^\[(active|done)\]\s+(\S+)\s+v(\d+)\s+(\S+)\s+(.+)$/); + if (!m) continue; + rules.push({ + status: m[1]!, + id: m[2]!, + version: parseInt(m[3]!, 10), + author: m[4]!, + text: m[5]!, + }); + } + return rules; +} + +class DashboardController { + private editorSync: EditorGraphSyncHandle | undefined; + private snapshot: GraphSnapshot | null = null; + + constructor( + private readonly webview: vscode.Webview, + private readonly disposables: vscode.Disposable[], + ) { + webview.options = { enableScripts: true }; + webview.html = getDashboardHtml(webview, vscode.Uri.file(__dirname)); + + disposables.push( + webview.onDidReceiveMessage((msg: WebviewInboundMessage) => { + void this.handleMessage(msg); + }), + ); + } + + dispose(): void { + this.editorSync?.dispose(); + } + + async refreshAll(): Promise { + await this.pushDashboardData(); + await this.pushSettings(); + await this.pushSessions(); + await this.pushRules(); + await this.pushSkills(); + } + + private post(message: Record): void { + void this.webview.postMessage(message); + } + + private async handleMessage(msg: WebviewInboundMessage): Promise { + try { + switch (msg.type) { + case "ready": + case "refresh": + await this.refreshAll(); + break; + case "setPane": + if (msg.pane === "graph" && this.snapshot) this.ensureEditorSync(); + break; + case "openSession": + if (msg.sessionId) { + const text = readSessionSummary(msg.sessionId); + const nextSteps = text ? extractNextSteps(text) : []; + this.post({ + type: "sessionSummary", + text: text ?? `No summary file found for session ${msg.sessionId}.`, + nextSteps, + }); + } + break; + case "toggleEmbeddings": { + const status = await runHivemindCli(["embeddings", "status"], workspaceRoot()); + const enabled = status.stdout.toLowerCase().includes("enabled: true"); + const cmd = enabled ? ["embeddings", "disable"] : ["embeddings", "enable"]; + const result = await runHivemindCli(cmd, workspaceRoot()); + await this.pushSettings(); + this.post({ + type: "actionResult", + target: "embeddings", + ok: result.ok, + message: result.ok ? (enabled ? "Embeddings disabled." : "Embeddings enabled.") : result.stderr, + }); + break; + } + case "buildGraph": { + const result = await runHivemindCli(["graph", "build"], workspaceRoot()); + this.post({ + type: "actionResult", + target: "graphBuild", + ok: result.ok, + message: result.ok ? "Graph build finished." : result.stderr || "Graph build failed.", + }); + if (result.ok) await this.pushDashboardData(); + break; + } + case "syncSkills": { + backfillCursorLinks(workspaceRoot()); + const state = syncSkillsToCursor(workspaceRoot()); + this.post({ + type: "actionResult", + target: "skillSync", + ok: state.erroredCount === 0, + message: `${state.syncedCount} synced, ${state.skippedCount} partial, ${state.erroredCount} failed.`, + }); + await this.pushSettings(); + await this.pushSkills(); + break; + } + case "rulesList": + await this.pushRules(); + break; + case "rulesAdd": + if (msg.text) { + const result = await runHivemindCli(["rules", "add", msg.text, "--scope", "team"], workspaceRoot()); + this.post({ + type: "actionResult", + target: "rules", + ok: result.ok, + message: result.ok ? "Rule added." : result.stderr, + }); + } + break; + case "rulesDone": + if (msg.ruleId) { + const result = await runHivemindCli(["rules", "done", msg.ruleId], workspaceRoot()); + this.post({ + type: "actionResult", + target: "rules", + ok: result.ok, + message: result.ok ? "Rule completed." : result.stderr, + }); + } + break; + case "rulesEdit": + if (msg.ruleId && msg.text) { + const result = await runHivemindCli(["rules", "edit", msg.ruleId, msg.text], workspaceRoot()); + this.post({ + type: "actionResult", + target: "rules", + ok: result.ok, + message: result.ok ? "Rule updated." : result.stderr, + }); + } + break; + case "promoteSkill": + if (msg.dirName) { + const skillName = msg.dirName.replace(/--[^/]+$/, ""); + const publishResult = await runHivemindCli( + ["skillify", "promote", skillName, "--scope", "team"], + workspaceRoot(), + ); + this.post({ + type: "actionResult", + target: "skillPromote", + ok: publishResult.ok, + message: publishResult.ok + ? `Skill "${skillName}" promoted to team. Teammates will pull it on their next session.` + : publishResult.stderr || "Promotion failed.", + }); + await this.pushSkills(); + } + break; + case "nextStepsPromote": + if (msg.text) { + const result = await runHivemindCli(["goal", "add", msg.text], workspaceRoot()); + this.post({ + type: "actionResult", + target: "nextStepsGoal", + ok: result.ok, + message: result.ok ? `Goal created: "${msg.text}"` : result.stderr || "Failed to create goal.", + }); + } + break; + case "switchOrg": + if (msg.orgName) { + const result = await runHivemindCli(["org", "switch", msg.orgName], workspaceRoot()); + this.post({ + type: "actionResult", + target: "orgSwitch", + ok: result.ok, + message: result.ok ? `Switched to org "${msg.orgName}".` : result.stderr || "Org switch failed.", + }); + if (result.ok) await this.pushSettings(); + } + break; + case "computeImpact": + if (this.snapshot) { + const impact = computeImpactOverlay(this.snapshot, workspaceRoot()); + this.post({ type: "impact", impact }); + } + break; + case "graphNodeClick": + if (msg.nodeId && this.snapshot) { + const node = this.snapshot.nodes.find((n) => n.id === msg.nodeId); + if (node) { + const opened = await openNodeInEditor(node, workspaceRoot()); + if (opened.message) { + void vscode.window.showInformationMessage(opened.message); + } + } + } + break; + default: + break; + } + } catch (err: unknown) { + logError("Dashboard message handler failed", err); + this.post({ type: "error", message: "Action failed." }); + } + } + + private ensureEditorSync(): void { + if (!this.snapshot || this.editorSync) return; + this.editorSync = startEditorToGraphSync(this.snapshot, workspaceRoot(), (nodeIds) => { + this.post({ type: "graphHighlight", nodeIds }); + }); + this.disposables.push({ dispose: () => this.editorSync?.dispose() }); + } + + private async pushDashboardData(): Promise { + const data = await loadDashboardData(workspaceRoot()); + this.snapshot = loadGraphSnapshotFromEnvelope(data); + if (!this.snapshot && data.graph?.snapshot) { + this.snapshot = parseGraphSnapshot(data.graph.snapshot); + } + this.post({ type: "dashboardData", data }); + if (this.snapshot) this.ensureEditorSync(); + } + + private async pushSettings(): Promise { + const auth = await detectAuthState(); + const health = await runHealthCheck(); + const emb = await runHivemindCli(["embeddings", "status"], workspaceRoot()); + const sync = syncSkillsToCursor(workspaceRoot()); + const healthSummary = health.dimensions.map((d) => `${d.label}: ${d.status}`).join(" · "); + this.post({ + type: "settings", + authLabel: formatIdentity(auth), + healthSummary, + embeddingsStatus: emb.ok ? emb.stdout.split("\n")[0] ?? "unknown" : "unavailable", + skillSyncSummary: `Last sync: ${sync.syncedCount} ok, ${sync.erroredCount} failed`, + graphBuildMessage: "", + }); + } + + private async pushSessions(): Promise { + const sessions = await loadRecentSessions(workspaceRoot()); + this.post({ type: "sessions", sessions }); + } + + private async pushRules(): Promise { + const result = await runHivemindCli(["rules", "list", "--status", "active", "--limit", "25"], workspaceRoot()); + const rules = result.ok ? parseRulesList(result.stdout) : []; + this.post({ type: "rules", rules }); + } + + private async pushSkills(): Promise { + const skills = listLocalSkillsForPromoter().map((s) => ({ + dirName: s.dirName, + label: skillDirLabel(s.dirName), + scope: s.scope, + path: s.path, + })); + this.post({ type: "skills", skills }); + } +} + +export class DashboardPanel { + public static currentPanel: DashboardPanel | undefined; + public static readonly viewType = "hivemind.dashboardPanel"; + + private readonly panel: vscode.WebviewPanel; + private readonly controller: DashboardController; + private readonly disposables: vscode.Disposable[] = []; + + public static createOrShow(extensionUri: vscode.Uri, _context: vscode.ExtensionContext): void { + const column = vscode.window.activeTextEditor?.viewColumn ?? vscode.ViewColumn.One; + + if (DashboardPanel.currentPanel) { + DashboardPanel.currentPanel.panel.reveal(column); + void DashboardPanel.currentPanel.controller.refreshAll(); + return; + } + + const panel = vscode.window.createWebviewPanel( + DashboardPanel.viewType, + "Hivemind Dashboard", + column, + { enableScripts: true, retainContextWhenHidden: true }, + ); + + DashboardPanel.currentPanel = new DashboardPanel(panel, extensionUri); + } + + private constructor(panel: vscode.WebviewPanel, _extensionUri: vscode.Uri) { + this.panel = panel; + this.controller = new DashboardController(panel.webview, this.disposables); + + this.panel.onDidDispose(() => this.dispose(), null, this.disposables); + this.panel.onDidChangeViewState( + (e) => { + if (e.webviewPanel.visible) void this.controller.refreshAll(); + }, + null, + this.disposables, + ); + + void this.controller.refreshAll(); + } + + public dispose(): void { + DashboardPanel.currentPanel = undefined; + this.controller.dispose(); + this.panel.dispose(); + while (this.disposables.length) { + const d = this.disposables.pop(); + if (d) d.dispose(); + } + } +} + +class DashboardViewProvider implements vscode.WebviewViewProvider { + private controller: DashboardController | undefined; + + constructor(private readonly context: vscode.ExtensionContext) {} + + resolveWebviewView( + webviewView: vscode.WebviewView, + _context: vscode.WebviewViewResolveContext, + _token: vscode.CancellationToken, + ): void { + const disposables: vscode.Disposable[] = []; + this.context.subscriptions.push(...disposables); + this.controller = new DashboardController(webviewView.webview, disposables); + webviewView.onDidChangeVisibility(() => { + if (webviewView.visible) void this.controller?.refreshAll(); + }); + void this.controller.refreshAll(); + } +} + +export function registerDashboardWebview(context: vscode.ExtensionContext): void { + context.subscriptions.push( + vscode.window.registerWebviewViewProvider("hivemind.dashboard", new DashboardViewProvider(context), { + webviewOptions: { retainContextWhenHidden: true }, + }), + ); +} diff --git a/cursor-extension/src/webview/data-bridge.ts b/cursor-extension/src/webview/data-bridge.ts new file mode 100644 index 00000000..95169322 --- /dev/null +++ b/cursor-extension/src/webview/data-bridge.ts @@ -0,0 +1,212 @@ +import { execFileSync } from "node:child_process"; +import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; +import { homedir } from "node:os"; +import { basename, join } from "node:path"; +import { credentialsPath } from "../utils/paths"; +import { readJson } from "../utils/fs-json"; + +const BYTES_PER_TOKEN = 4; +const SAVINGS_MULTIPLIER = 1.7; + +export interface DashboardKpis { + tokensSaved: number | null; + tokensSource: "org" | "local" | "none"; + skillsCreated: number; + memorySearches: number; + sessionsCount: number | null; + userTokensSaved: number | null; + fetchedAt?: string; +} + +export interface DashboardGraphSummary { + commitSha: string | null; + snapshotPath: string; + nodeCount: number; + edgeCount: number; + snapshot: unknown; +} + +export interface DashboardDataEnvelope { + repoKey: string; + repoProject: string; + generatedAt: string; + kpis: DashboardKpis; + graph: DashboardGraphSummary | null; +} + +export interface RecentSession { + sessionId: string; + endedAt: string; + memorySearchCount: number; +} + +function statsFilePath(): string { + return join(homedir(), ".deeplake", "usage-stats.jsonl"); +} + +function readUsageRecords(): Array<{ endedAt: string; sessionId: string; memorySearchBytes: number; memorySearchCount: number }> { + try { + if (!existsSync(statsFilePath())) return []; + const out: Array<{ endedAt: string; sessionId: string; memorySearchBytes: number; memorySearchCount: number }> = []; + for (const line of readFileSync(statsFilePath(), "utf-8").split("\n")) { + if (!line.trim()) continue; + try { + const rec = JSON.parse(line) as Partial<{ endedAt: string; sessionId: string; memorySearchBytes: number; memorySearchCount: number }>; + if (rec.endedAt && rec.sessionId) { + out.push({ + endedAt: rec.endedAt, + sessionId: rec.sessionId, + memorySearchBytes: rec.memorySearchBytes ?? 0, + memorySearchCount: rec.memorySearchCount ?? 0, + }); + } + } catch { + /* skip bad line */ + } + } + return out; + } catch { + return []; + } +} + +function countSkills(userName?: string): number { + const skillsRoot = join(homedir(), ".claude", "skills"); + if (!existsSync(skillsRoot)) return 0; + let count = 0; + for (const name of readdirSync(skillsRoot)) { + if (!name.includes("--")) continue; + if (userName && !name.endsWith(`--${userName}`)) continue; + count++; + } + return count; +} + +function bytesToSavedTokens(bytes: number): number { + if (!Number.isFinite(bytes) || bytes <= 0) return 0; + return (SAVINGS_MULTIPLIER - 1) * (bytes / BYTES_PER_TOKEN); +} + +function deriveProjectKey(cwd: string): { key: string; project: string } { + try { + const out = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf-8" }).trim(); + const key = Buffer.from(out).toString("hex").slice(0, 16); + return { key, project: basename(out) }; + } catch { + const key = Buffer.from(cwd).toString("hex").slice(0, 16); + return { key, project: basename(cwd) }; + } +} + +function graphsRoot(): string { + return process.env.HIVEMIND_GRAPHS_HOME ?? join(homedir(), ".hivemind", "graphs"); +} + +function resolveSnapshot(repoDir: string): DashboardGraphSummary | null { + const snapshotsDir = join(repoDir, "snapshots"); + if (!existsSync(snapshotsDir)) return null; + let snapshotPath: string | null = null; + const pointer = join(repoDir, "latest-commit.txt"); + if (existsSync(pointer)) { + const sha = readFileSync(pointer, "utf-8").trim(); + const candidate = join(snapshotsDir, `${sha}.json`); + if (sha && existsSync(candidate)) snapshotPath = candidate; + } + if (!snapshotPath) { + const candidates = readdirSync(snapshotsDir) + .filter((n) => n.endsWith(".json")) + .map((n) => ({ full: join(snapshotsDir, n), mtime: statSync(join(snapshotsDir, n)).mtimeMs })) + .sort((a, b) => b.mtime - a.mtime); + if (candidates[0]) snapshotPath = candidates[0].full; + } + if (!snapshotPath) return null; + try { + const parsed = JSON.parse(readFileSync(snapshotPath, "utf-8")) as { + nodes?: unknown[]; + links?: unknown[]; + graph?: { commit_sha?: string | null }; + }; + if (!Array.isArray(parsed.nodes) || !Array.isArray(parsed.links)) return null; + return { + commitSha: parsed.graph?.commit_sha ?? null, + snapshotPath, + nodeCount: parsed.nodes.length, + edgeCount: parsed.links.length, + snapshot: parsed, + }; + } catch { + return null; + } +} + +export async function loadDashboardData(cwd: string): Promise { + const { key: repoKey, project: repoProject } = deriveProjectKey(cwd); + const creds = readJson<{ userName?: string }>(credentialsPath()); + const records = readUsageRecords(); + const localBytes = records.reduce((s, r) => s + r.memorySearchBytes, 0); + const localCount = records.reduce((s, r) => s + r.memorySearchCount, 0); + const skillsCreated = countSkills(creds?.userName); + + let kpis: DashboardKpis; + if (records.length === 0 && !creds) { + kpis = { + tokensSaved: null, + tokensSource: "none", + skillsCreated, + memorySearches: 0, + sessionsCount: null, + userTokensSaved: null, + fetchedAt: new Date().toISOString(), + }; + } else { + const saved = bytesToSavedTokens(localBytes); + kpis = { + tokensSaved: saved, + tokensSource: "local", + skillsCreated, + memorySearches: localCount, + sessionsCount: records.length, + userTokensSaved: saved, + fetchedAt: new Date().toISOString(), + }; + } + + const graph = resolveSnapshot(join(graphsRoot(), repoKey)); + return { + repoKey, + repoProject, + generatedAt: new Date().toISOString(), + kpis, + graph, + }; +} + +export async function loadRecentSessions(_cwd: string): Promise { + return readUsageRecords() + .slice(-20) + .reverse() + .map((r) => ({ + sessionId: r.sessionId, + endedAt: r.endedAt, + memorySearchCount: r.memorySearchCount, + })); +} + +export async function runHivemindCli(args: string[], cwd: string): Promise<{ ok: boolean; stdout: string; stderr: string }> { + try { + const stdout = execFileSync("hivemind", args, { + encoding: "utf-8", + cwd, + timeout: 300_000, + env: { ...process.env }, + }); + return { ok: true, stdout, stderr: "" }; + } catch (err: unknown) { + const e = err as { stdout?: string; stderr?: string; message?: string }; + return { + ok: false, + stdout: e.stdout ?? "", + stderr: e.stderr ?? e.message ?? "Command failed", + }; + } +} diff --git a/cursor-extension/src/webview/html/dashboard-shell.ts b/cursor-extension/src/webview/html/dashboard-shell.ts new file mode 100644 index 00000000..125dc58f --- /dev/null +++ b/cursor-extension/src/webview/html/dashboard-shell.ts @@ -0,0 +1,483 @@ +import { randomBytes } from "node:crypto"; +import * as vscode from "vscode"; + +const D3_CDN = "https://d3js.org/d3.v7.min.js"; + +function getNonce(): string { + return randomBytes(16).toString("hex"); +} + +function cspSource(webview: vscode.Webview): string { + return webview.cspSource; +} + +/** Self-contained dashboard HTML for panel or sidebar webview. */ +export function getDashboardHtml(webview: vscode.Webview, extensionUri: vscode.Uri): string { + const nonce = getNonce(); + const csp = cspSource(webview); + + return ` + + + + + + Hivemind Dashboard + + + +
+

Hivemind

+ +
+ +
+
+
+

+
+
+

Org & health

+

+

+
+ + +
+

+

Embeddings

+
+ Loading… + +
+

Codebase graph

+ +

+

Skill sync

+ +

+
+
+
    + +
    +
    +
    + + +
    + + +

    +
    +
    +
    + + +
    +
      +
      +
      +

      Local skills available under Claude/Cursor skill directories. Use "Promote to team" to share a skill so teammates pull it on their next session.

      +
      +

      +
      +
      + + + +`; +} diff --git a/cursor-extension/tsconfig.json b/cursor-extension/tsconfig.json new file mode 100644 index 00000000..b02896bb --- /dev/null +++ b/cursor-extension/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2022", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": "src", + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "paths": { + "@hivemind/*": ["../src/*"] + }, + "baseUrl": "." + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/cursor-extension/webpack.config.js b/cursor-extension/webpack.config.js new file mode 100644 index 00000000..6dd4c3d1 --- /dev/null +++ b/cursor-extension/webpack.config.js @@ -0,0 +1,36 @@ +//@ts-check +"use strict"; + +const path = require("path"); + +/** @type {import('webpack').Configuration} */ +module.exports = { + target: "node", + mode: "none", + entry: "./src/extension.ts", + output: { + path: path.resolve(__dirname, "dist"), + filename: "extension.js", + libraryTarget: "commonjs2", + devtoolModuleFilenameTemplate: "../[resource-path]", + }, + devtool: "source-map", + externals: { + vscode: "commonjs vscode", + }, + resolve: { + extensions: [".ts", ".js"], + alias: { + "@hivemind": path.resolve(__dirname, "../src"), + }, + }, + module: { + rules: [ + { + test: /\.ts$/, + exclude: /node_modules/, + use: [{ loader: "ts-loader" }], + }, + ], + }, +}; diff --git a/src/skillify/agent-roots.ts b/src/skillify/agent-roots.ts index e1efc0ed..b4afc05b 100644 --- a/src/skillify/agent-roots.ts +++ b/src/skillify/agent-roots.ts @@ -24,8 +24,10 @@ * canonical write location IS that path; symlinking a skill into itself * would be a no-op at best and a self-referential loop at worst. * - * Cursor has no native skill discovery (only hooks/rules), so it is not - * a candidate. + * Cursor discovers skills under `~/.cursor/skills-cursor/` (global) and + * `/.cursor/skills/` (project). When `~/.cursor` exists, the + * global Cursor root is included; when `projectRoot` is passed, the + * project Cursor root is included as well. */ import { existsSync } from "node:fs"; @@ -45,11 +47,12 @@ import { join } from "node:path"; * `fanOutSymlinks` upstream calls `mkdirSync(dirname(link), { recursive })` * before each symlink, so the directory is created on first fan-out. */ -function resolveDetected(home: string): string[] { +function resolveDetected(home: string, projectRoot?: string): string[] { const out: string[] = []; const codexInstalled = existsSync(join(home, ".codex")); const piInstalled = existsSync(join(home, ".pi", "agent")); const hermesInstalled = existsSync(join(home, ".hermes")); + const cursorInstalled = existsSync(join(home, ".cursor")); // agentskills.io shared root — codex creates it, pi co-consumes it. if (codexInstalled || piInstalled) { @@ -63,6 +66,12 @@ function resolveDetected(home: string): string[] { if (piInstalled) { out.push(join(home, ".pi", "agent", "skills")); } + if (cursorInstalled) { + out.push(join(home, ".cursor", "skills-cursor")); + if (projectRoot) { + out.push(join(projectRoot, ".cursor", "skills")); + } + } return out; } @@ -79,6 +88,7 @@ function resolveDetected(home: string): string[] { export function detectAgentSkillsRoots( canonicalRoot: string, home: string = homedir(), + projectRoot?: string, ): string[] { - return resolveDetected(home).filter(p => p !== canonicalRoot); + return resolveDetected(home, projectRoot).filter((p) => p !== canonicalRoot); } diff --git a/tests/claude-code/skillify-agent-roots.test.ts b/tests/claude-code/skillify-agent-roots.test.ts index 06ae4eb7..b113ea02 100644 --- a/tests/claude-code/skillify-agent-roots.test.ts +++ b/tests/claude-code/skillify-agent-roots.test.ts @@ -20,6 +20,7 @@ const canonical = (home: string) => join(home, ".claude", "skills"); function installCodex(home: string): void { mkdirSync(join(home, ".codex"), { recursive: true }); } function installHermes(home: string): void { mkdirSync(join(home, ".hermes"), { recursive: true }); } function installPi(home: string): void { mkdirSync(join(home, ".pi", "agent"), { recursive: true }); } +function installCursor(home: string): void { mkdirSync(join(home, ".cursor"), { recursive: true }); } describe("detectAgentSkillsRoots", () => { it("returns empty when no agent config dirs exist", () => { @@ -102,6 +103,23 @@ describe("detectAgentSkillsRoots", () => { expect(result).toContain(join(tmpHome, ".pi", "agent", "skills")); }); + it("includes ~/.cursor/skills-cursor when cursor is installed", () => { + installCursor(tmpHome); + expect(detectAgentSkillsRoots(canonical(tmpHome), tmpHome)).toEqual([ + join(tmpHome, ".cursor", "skills-cursor"), + ]); + }); + + it("includes project .cursor/skills when cursor is installed and projectRoot is passed", () => { + installCursor(tmpHome); + const project = join(tmpHome, "my-repo"); + mkdirSync(project, { recursive: true }); + expect(detectAgentSkillsRoots(canonical(tmpHome), tmpHome, project)).toEqual([ + join(tmpHome, ".cursor", "skills-cursor"), + join(project, ".cursor", "skills"), + ]); + }); + it("ignores a regular file at ~/.codex / ~/.hermes / ~/.pi/agent (parent must be a directory)", () => { // existsSync returns true for files too, so a stray ~/.codex file // (e.g. from `touch ~/.codex` by mistake) shouldn't trick the