=e-1){var s=u[n];return s.x0=i,s.y0=o,s.x1=a,void(s.y1=c)}var l=f[n],h=r/2+l,d=n+1,p=e-1;for(;d>>1;f[g]c-o){var _=r?(i*v+a*y)/r:a;t(n,d,y,i,o,_,c),t(d,e,v,_,o,a,c)}else{var b=r?(o*v+c*y)/r:c;t(n,d,y,i,o,a,b),t(d,e,v,i,b,a,c)}}(0,c,t.value,n,e,r,i)},t.treemapDice=Ap,t.treemapResquarify=Lp,t.treemapSlice=Ip,t.treemapSliceDice=function(t,n,e,r,i){(1&t.depth?Ip:Ap)(t,n,e,r,i)},t.treemapSquarify=Yp,t.tsv=Mc,t.tsvFormat=lc,t.tsvFormatBody=hc,t.tsvFormatRow=pc,t.tsvFormatRows=dc,t.tsvFormatValue=gc,t.tsvParse=fc,t.tsvParseRows=sc,t.union=function(...t){const n=new InternSet;for(const e of t)for(const t of e)n.add(t);return n},t.unixDay=_y,t.unixDays=by,t.utcDay=yy,t.utcDays=vy,t.utcFriday=By,t.utcFridays=Vy,t.utcHour=hy,t.utcHours=dy,t.utcMillisecond=Wg,t.utcMilliseconds=Zg,t.utcMinute=cy,t.utcMinutes=fy,t.utcMonday=qy,t.utcMondays=jy,t.utcMonth=Qy,t.utcMonths=Jy,t.utcSaturday=Yy,t.utcSaturdays=Wy,t.utcSecond=iy,t.utcSeconds=oy,t.utcSunday=Fy,t.utcSundays=Ly,t.utcThursday=Oy,t.utcThursdays=Gy,t.utcTickInterval=av,t.utcTicks=ov,t.utcTuesday=Uy,t.utcTuesdays=Hy,t.utcWednesday=Iy,t.utcWednesdays=Xy,t.utcWeek=Fy,t.utcWeeks=Ly,t.utcYear=ev,t.utcYears=rv,t.variance=x,t.version="7.9.0",t.window=pn,t.xml=Sc,t.zip=function(){return gt(arguments)},t.zoom=function(){var t,n,e,r=Ew,i=Nw,o=zw,a=Cw,u=Pw,c=[0,1/0],f=[[-1/0,-1/0],[1/0,1/0]],s=250,l=ri,h=$t("start","zoom","end"),d=500,p=150,g=0,y=10;function v(t){t.property("__zoom",kw).on("wheel.zoom",T,{passive:!1}).on("mousedown.zoom",A).on("dblclick.zoom",S).filter(u).on("touchstart.zoom",E).on("touchmove.zoom",N).on("touchend.zoom touchcancel.zoom",k).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function _(t,n){return(n=Math.max(c[0],Math.min(c[1],n)))===t.k?t:new ww(n,t.x,t.y)}function b(t,n,e){var r=n[0]-e[0]*t.k,i=n[1]-e[1]*t.k;return r===t.x&&i===t.y?t:new ww(t.k,r,i)}function m(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function x(t,n,e,r){t.on("start.zoom",(function(){w(this,arguments).event(r).start()})).on("interrupt.zoom end.zoom",(function(){w(this,arguments).event(r).end()})).tween("zoom",(function(){var t=this,o=arguments,a=w(t,o).event(r),u=i.apply(t,o),c=null==e?m(u):"function"==typeof e?e.apply(t,o):e,f=Math.max(u[1][0]-u[0][0],u[1][1]-u[0][1]),s=t.__zoom,h="function"==typeof n?n.apply(t,o):n,d=l(s.invert(c).concat(f/s.k),h.invert(c).concat(f/h.k));return function(t){if(1===t)t=h;else{var n=d(t),e=f/n[2];t=new ww(e,c[0]-n[0]*e,c[1]-n[1]*e)}a.zoom(null,t)}}))}function w(t,n,e){return!e&&t.__zooming||new M(t,n)}function M(t,n){this.that=t,this.args=n,this.active=0,this.sourceEvent=null,this.extent=i.apply(t,n),this.taps=0}function T(t,...n){if(r.apply(this,arguments)){var e=w(this,n).event(t),i=this.__zoom,u=Math.max(c[0],Math.min(c[1],i.k*Math.pow(2,a.apply(this,arguments)))),s=ne(t);if(e.wheel)e.mouse[0][0]===s[0]&&e.mouse[0][1]===s[1]||(e.mouse[1]=i.invert(e.mouse[0]=s)),clearTimeout(e.wheel);else{if(i.k===u)return;e.mouse=[s,i.invert(s)],Gi(this),e.start()}Sw(t),e.wheel=setTimeout((function(){e.wheel=null,e.end()}),p),e.zoom("mouse",o(b(_(i,u),e.mouse[0],e.mouse[1]),e.extent,f))}}function A(t,...n){if(!e&&r.apply(this,arguments)){var i=t.currentTarget,a=w(this,n,!0).event(t),u=Zn(t.view).on("mousemove.zoom",(function(t){if(Sw(t),!a.moved){var n=t.clientX-s,e=t.clientY-l;a.moved=n*n+e*e>g}a.event(t).zoom("mouse",o(b(a.that.__zoom,a.mouse[0]=ne(t,i),a.mouse[1]),a.extent,f))}),!0).on("mouseup.zoom",(function(t){u.on("mousemove.zoom mouseup.zoom",null),ue(t.view,a.moved),Sw(t),a.event(t).end()}),!0),c=ne(t,i),s=t.clientX,l=t.clientY;ae(t.view),Aw(t),a.mouse=[c,this.__zoom.invert(c)],Gi(this),a.start()}}function S(t,...n){if(r.apply(this,arguments)){var e=this.__zoom,a=ne(t.changedTouches?t.changedTouches[0]:t,this),u=e.invert(a),c=e.k*(t.shiftKey?.5:2),l=o(b(_(e,c),a,u),i.apply(this,n),f);Sw(t),s>0?Zn(this).transition().duration(s).call(x,l,a,t):Zn(this).call(v.transform,l,a,t)}}function E(e,...i){if(r.apply(this,arguments)){var o,a,u,c,f=e.touches,s=f.length,l=w(this,i,e.changedTouches.length===s).event(e);for(Aw(e),a=0;a
+
+
+
diff --git a/harnesses/cursor/extension/package-lock.json b/harnesses/cursor/extension/package-lock.json
new file mode 100644
index 00000000..4c7c73be
--- /dev/null
+++ b/harnesses/cursor/extension/package-lock.json
@@ -0,0 +1,1693 @@
+{
+ "name": "hivemind-cursor-extension",
+ "version": "0.1.5",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "hivemind-cursor-extension",
+ "version": "0.1.5",
+ "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/harnesses/cursor/extension/package.json b/harnesses/cursor/extension/package.json
new file mode 100644
index 00000000..abb14f81
--- /dev/null
+++ b/harnesses/cursor/extension/package.json
@@ -0,0 +1,85 @@
+{
+ "name": "hivemind-cursor-extension",
+ "displayName": "Hivemind for Cursor",
+ "description": "Shared memory, health checks, dashboard, and codebase graph inside Cursor",
+ "version": "0.1.5",
+ "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.unwireHooks",
+ "title": "Hivemind: Unwire 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/harnesses/cursor/extension/scripts/lib/deeplake.mjs b/harnesses/cursor/extension/scripts/lib/deeplake.mjs
new file mode 100644
index 00000000..8ed685ff
--- /dev/null
+++ b/harnesses/cursor/extension/scripts/lib/deeplake.mjs
@@ -0,0 +1,140 @@
+/**
+ * Self-contained Deeplake data helper for the Cursor extension loaders.
+ *
+ * The loader scripts run via plain `node` spawned from the packaged
+ * extension. The core Hivemind CLI source (repo-root `src/`) is neither
+ * shipped in the vsix nor compiled to `.js`, so the old loaders that did
+ * `import ... from "../../../../src/*.js"` always failed at runtime. This
+ * module reimplements just the small slice of behaviour the loaders need,
+ * with no dependency outside the scripts directory: reading credentials,
+ * running a SQL query against the Deeplake HTTP endpoint, escaping SQL
+ * literals, and deriving the per-repo graph key the core uses.
+ */
+import { readFileSync } from "node:fs";
+import { homedir } from "node:os";
+import { join } from "node:path";
+import { execSync } from "node:child_process";
+import { createHash } from "node:crypto";
+
+const DEFAULT_API_URL = "https://api.deeplake.ai";
+
+export function loadCreds() {
+ try {
+ const raw = readFileSync(join(homedir(), ".deeplake", "credentials.json"), "utf-8");
+ const creds = JSON.parse(raw);
+ if (!creds || !creds.token || !creds.orgId) return null;
+ return {
+ token: creds.token,
+ orgId: creds.orgId,
+ orgName: creds.orgName ?? creds.orgId,
+ userName: creds.userName ?? "",
+ workspaceId: creds.workspaceId ?? "default",
+ apiUrl: creds.apiUrl ?? DEFAULT_API_URL,
+ };
+ } catch {
+ return null;
+ }
+}
+
+/** Table names, matching core `src/config.ts` defaults plus env overrides. */
+export function tableNames() {
+ return {
+ memory: process.env.HIVEMIND_TABLE ?? "memory",
+ sessions: process.env.HIVEMIND_SESSIONS_TABLE ?? "sessions",
+ rules: process.env.HIVEMIND_RULES_TABLE ?? "hivemind_rules",
+ goals: process.env.HIVEMIND_GOALS_TABLE ?? "hivemind_goals",
+ };
+}
+
+/** Escape a string for a single-quoted SQL literal. Mirrors src/utils/sql.ts. */
+export function sqlStr(value) {
+ return String(value)
+ .replace(/\\/g, "\\\\")
+ .replace(/'/g, "''")
+ .replace(/\0/g, "")
+ // eslint-disable-next-line no-control-regex
+ .replace(/[\x01-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
+}
+
+/** Validate a SQL identifier (table/column). Mirrors src/utils/sql.ts. */
+export function sqlIdent(name) {
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
+ throw new Error(`Invalid SQL identifier: ${JSON.stringify(name)}`);
+ }
+ return name;
+}
+
+/** True when the API error means the table has not been created yet. */
+export function isMissingTableError(message) {
+ return /does not exist|no such table|not found/i.test(String(message ?? ""));
+}
+
+/**
+ * Run a SQL query against the Deeplake query endpoint and return rows as
+ * plain objects. Mirrors the request shape in src/deeplake-api.ts.
+ */
+export async function query(creds, sql, timeoutMs = 8000) {
+ const resp = await fetch(`${creds.apiUrl}/workspaces/${creds.workspaceId}/tables/query`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${creds.token}`,
+ "Content-Type": "application/json",
+ "X-Activeloop-Org-Id": creds.orgId,
+ "X-Deeplake-Client": "hivemind",
+ },
+ signal: AbortSignal.timeout(timeoutMs),
+ body: JSON.stringify({ query: sql }),
+ });
+ if (!resp.ok) {
+ const text = await resp.text().catch(() => "");
+ throw new Error(`Query failed: ${resp.status}: ${text.slice(0, 200)}`);
+ }
+ const raw = await resp.json().catch(() => null);
+ if (!raw || !Array.isArray(raw.rows) || !Array.isArray(raw.columns)) return [];
+ return raw.rows.map((row) => Object.fromEntries(raw.columns.map((col, i) => [col, row[i]])));
+}
+
+/** Collapse the surface forms of a git remote URL. Mirrors src/utils/repo-identity.ts. */
+export function normalizeGitRemoteUrl(url) {
+ let s = String(url).trim();
+ const schemeMatch = s.match(/^([a-z][a-z0-9+.-]*):\/\//i);
+ const scheme = schemeMatch ? schemeMatch[1].toLowerCase() : null;
+ if (schemeMatch) s = s.slice(schemeMatch[0].length);
+ if (!scheme) {
+ const scp = s.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/);
+ if (scp) s = `${scp[1]}/${scp[2]}`;
+ }
+ s = s.replace(/^[^@/]+@/, "");
+ const defaultPorts = { http: "80", https: "443", ssh: "22", git: "9418" };
+ if (scheme && defaultPorts[scheme]) {
+ s = s.replace(new RegExp(`^([^/]+):${defaultPorts[scheme]}(/|$)`), "$1$2");
+ }
+ s = s.replace(/\.git\/?$/i, "");
+ s = s.replace(/\/+$/, "");
+ return s.toLowerCase();
+}
+
+/**
+ * Stable per-repo key: sha1 of the normalized git remote (fallback to the
+ * absolute cwd), first 16 hex chars. Mirrors core deriveProjectKey so the
+ * extension resolves the SAME `~/.hivemind/graphs/` dir the CLI writes.
+ */
+export function deriveProjectKey(cwd) {
+ let signature = null;
+ try {
+ const raw = execSync("git config --get remote.origin.url", {
+ cwd,
+ encoding: "utf-8",
+ stdio: ["ignore", "pipe", "ignore"],
+ }).trim();
+ signature = raw ? normalizeGitRemoteUrl(raw) : null;
+ } catch {
+ signature = null;
+ }
+ const input = signature ?? cwd;
+ return createHash("sha1").update(input).digest("hex").slice(0, 16);
+}
+
+export function graphsHome() {
+ return process.env.HIVEMIND_GRAPHS_HOME ?? join(homedir(), ".hivemind", "graphs");
+}
diff --git a/harnesses/cursor/extension/scripts/load-dashboard.mjs b/harnesses/cursor/extension/scripts/load-dashboard.mjs
new file mode 100644
index 00000000..d5cd31d5
--- /dev/null
+++ b/harnesses/cursor/extension/scripts/load-dashboard.mjs
@@ -0,0 +1,305 @@
+/**
+ * Build the dashboard data envelope (KPIs + codebase graph snapshot).
+ *
+ * Self-contained: resolves the per-repo graph key the same way the core CLI
+ * does (see lib/deeplake.mjs deriveProjectKey) so it finds the snapshot the
+ * `hivemind graph build` command actually wrote under
+ * ~/.hivemind/graphs//snapshots. KPIs come from the org-stats cache the
+ * CLI maintains, falling back to local usage records, then to an empty state.
+ * Prints a DashboardDataEnvelope JSON to stdout.
+ */
+import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
+import { homedir } from "node:os";
+import { basename, dirname, join } from "node:path";
+import { loadCreds, deriveProjectKey, graphsHome, query, sqlStr, sqlIdent, tableNames } from "./lib/deeplake.mjs";
+
+const cwd = process.argv[2] || process.cwd();
+
+const BYTES_PER_TOKEN = 4;
+const SAVINGS_MULTIPLIER = 1.7;
+
+function bytesToSavedTokens(bytes) {
+ if (!Number.isFinite(bytes) || bytes <= 0) return 0;
+ return (SAVINGS_MULTIPLIER - 1) * (bytes / BYTES_PER_TOKEN);
+}
+
+function countUserGeneratedSkills(userName) {
+ if (!userName) return 0;
+ const dir = join(homedir(), ".claude", "skills");
+ if (!existsSync(dir)) return 0;
+ const suffix = `--${userName}`;
+ try {
+ let count = 0;
+ for (const name of readdirSync(dir)) {
+ const idx = name.lastIndexOf(suffix);
+ if (idx > 0 && idx + suffix.length === name.length) count += 1;
+ }
+ return count;
+ } catch {
+ return 0;
+ }
+}
+
+function readUsageRecords() {
+ const path = join(homedir(), ".deeplake", "usage-stats.jsonl");
+ if (!existsSync(path)) return [];
+ const out = [];
+ try {
+ for (const line of readFileSync(path, "utf-8").split("\n")) {
+ const trimmed = line.trim();
+ if (!trimmed) continue;
+ try {
+ const rec = JSON.parse(trimmed);
+ if (typeof rec.endedAt === "string" && typeof rec.sessionId === "string") {
+ out.push({
+ memorySearchBytes: typeof rec.memorySearchBytes === "number" ? rec.memorySearchBytes : 0,
+ memorySearchCount: typeof rec.memorySearchCount === "number" ? rec.memorySearchCount : 0,
+ });
+ }
+ } catch {
+ /* skip bad line */
+ }
+ }
+ } catch {
+ return [];
+ }
+ return out;
+}
+
+const STATS_CACHE_TTL_MS = 3600_000;
+
+function statsCachePath() {
+ return join(homedir(), ".deeplake", "hivemind-stats-cache.json");
+}
+
+function statsScopeKey(creds) {
+ return JSON.stringify({
+ apiUrl: creds.apiUrl ?? "https://api.deeplake.ai",
+ orgId: creds.orgId ?? "",
+ userName: creds.userName ?? "",
+ });
+}
+
+function nonNegNumber(value) {
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
+}
+
+function scopeFromServer(scope) {
+ return {
+ sessionsCount: nonNegNumber(scope?.sessions_count),
+ memoryRecallCount: nonNegNumber(scope?.memory_recall_count),
+ memorySearchBytes: nonNegNumber(scope?.memory_search_bytes),
+ };
+}
+
+function readStatsCache(scopeKey) {
+ try {
+ if (!existsSync(statsCachePath())) return {};
+ const parsed = JSON.parse(readFileSync(statsCachePath(), "utf-8"));
+ if (!parsed || parsed.scopeKey !== scopeKey || typeof parsed.fetchedAt !== "number") return {};
+ if (!parsed.data?.org) return {};
+ const age = Date.now() - parsed.fetchedAt;
+ if (age >= 0 && age < STATS_CACHE_TTL_MS) return { fresh: parsed.data, fetchedAt: parsed.fetchedAt };
+ return { stale: parsed.data, fetchedAt: parsed.fetchedAt };
+ } catch {
+ return {};
+ }
+}
+
+function writeStatsCache(scopeKey, data) {
+ try {
+ mkdirSync(dirname(statsCachePath()), { recursive: true });
+ writeFileSync(statsCachePath(), JSON.stringify({ fetchedAt: Date.now(), scopeKey, data }), "utf-8");
+ } catch {
+ /* best-effort cache */
+ }
+}
+
+/**
+ * Resolve org/user activity stats the same way the core CLI does: fresh
+ * cache first, then a live `GET /me/hivemind-stats`, then stale cache.
+ * Returns null when no creds or the endpoint is unreachable with no cache.
+ */
+async function fetchOrgStats(creds) {
+ if (!creds?.token) return null;
+ const apiUrl = creds.apiUrl ?? "https://api.deeplake.ai";
+ const scopeKey = statsScopeKey(creds);
+ const { fresh, stale, fetchedAt } = readStatsCache(scopeKey);
+ if (fresh) {
+ return { org: fresh.org, user: fresh.user ?? fresh.org, fetchedAt: new Date(fetchedAt).toISOString(), stale: false, offline: false };
+ }
+ try {
+ const resp = await fetch(`${apiUrl}/me/hivemind-stats`, {
+ headers: {
+ Authorization: `Bearer ${creds.token}`,
+ ...(creds.orgId ? { "X-Activeloop-Org-Id": creds.orgId } : {}),
+ },
+ signal: AbortSignal.timeout(8000),
+ });
+ if (resp.ok) {
+ const body = await resp.json().catch(() => null);
+ if (body && typeof body === "object") {
+ const data = { org: scopeFromServer(body.org), user: scopeFromServer(body.user) };
+ writeStatsCache(scopeKey, data);
+ return { org: data.org, user: data.user, fetchedAt: new Date().toISOString(), stale: false, offline: false };
+ }
+ }
+ } catch {
+ /* fall through to stale */
+ }
+ if (stale) {
+ return { org: stale.org, user: stale.user ?? stale.org, fetchedAt: new Date(fetchedAt).toISOString(), stale: true, offline: true };
+ }
+ return null;
+}
+
+/** Sum the locally-tracked recall events (count + bytes delivered). */
+function readRecallEvents() {
+ const path = join(homedir(), ".deeplake", "recall-events.jsonl");
+ if (!existsSync(path)) return { count: 0, bytes: 0 };
+ let count = 0;
+ let bytes = 0;
+ try {
+ for (const line of readFileSync(path, "utf-8").split("\n")) {
+ const trimmed = line.trim();
+ if (!trimmed) continue;
+ try {
+ const rec = JSON.parse(trimmed);
+ if (typeof rec.bytes === "number" && rec.bytes > 0) {
+ count += 1;
+ bytes += rec.bytes;
+ }
+ } catch {
+ /* skip bad line */
+ }
+ }
+ } catch {
+ return { count: 0, bytes: 0 };
+ }
+ return { count, bytes };
+}
+
+/** Real distinct-session count for this user from the sessions table. */
+async function fetchDistinctSessions(creds) {
+ if (!creds || !creds.userName) return null;
+ try {
+ const table = sqlIdent(tableNames().sessions);
+ const rows = await query(
+ creds,
+ `SELECT COUNT(DISTINCT path) AS c FROM "${table}" WHERE author = '${sqlStr(creds.userName)}'`,
+ );
+ const c = rows && rows[0] ? Number(rows[0].c) : NaN;
+ return Number.isFinite(c) ? c : null;
+ } catch {
+ return null;
+ }
+}
+
+function resolveSnapshot(repoDir) {
+ const snapshotsDir = join(repoDir, "snapshots");
+ if (!existsSync(snapshotsDir)) return null;
+ let snapshotPath = null;
+ const pointer = join(repoDir, "latest-commit.txt");
+ if (existsSync(pointer)) {
+ try {
+ const sha = readFileSync(pointer, "utf-8").trim();
+ const candidate = join(snapshotsDir, `${sha}.json`);
+ if (sha && existsSync(candidate)) snapshotPath = candidate;
+ } catch {
+ /* fall through to newest-file scan */
+ }
+ }
+ if (!snapshotPath) {
+ try {
+ 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;
+ } catch {
+ return null;
+ }
+ }
+ if (!snapshotPath) return null;
+ try {
+ const parsed = JSON.parse(readFileSync(snapshotPath, "utf-8"));
+ 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;
+ }
+}
+
+const creds = loadCreds();
+const repoKey = deriveProjectKey(cwd);
+const repoProject = basename(cwd);
+const graph = resolveSnapshot(join(graphsHome(), repoKey));
+const skillsCreated = countUserGeneratedSkills(creds?.userName);
+
+// Sessions: real distinct-session count for this user from the sessions
+// table (consistent with the Sessions tab), with org rollup as a fallback.
+const distinctSessions = await fetchDistinctSessions(creds);
+// Memory recall: tracked locally by the pre-tool-use hook into
+// recall-events.jsonl. The server rollup does not yet count Cursor recalls,
+// so this local store is the source of truth for memory-search / tokens-saved.
+const recall = readRecallEvents();
+const org = await fetchOrgStats(creds);
+
+let sessionsCount = distinctSessions;
+if (sessionsCount == null && org) sessionsCount = org.org.sessionsCount ?? null;
+
+let kpis;
+if (recall.count > 0) {
+ kpis = {
+ tokensSaved: bytesToSavedTokens(recall.bytes),
+ tokensSource: "local",
+ skillsCreated,
+ memorySearches: recall.count,
+ sessionsCount,
+ userTokensSaved: bytesToSavedTokens(recall.bytes),
+ orgStatsFetchedAt: null,
+ orgStatsStale: false,
+ orgStatsOffline: false,
+ };
+} else if (org && ((org.org.memoryRecallCount ?? 0) > 0 || (org.org.memorySearchBytes ?? 0) > 0)) {
+ kpis = {
+ tokensSaved: bytesToSavedTokens(org.org.memorySearchBytes ?? 0),
+ tokensSource: "org",
+ skillsCreated,
+ memorySearches: org.org.memoryRecallCount ?? 0,
+ sessionsCount,
+ userTokensSaved: bytesToSavedTokens((org.user ?? org.org).memorySearchBytes ?? 0),
+ orgStatsFetchedAt: org.fetchedAt,
+ orgStatsStale: org.stale,
+ orgStatsOffline: org.offline,
+ };
+} else {
+ // Sessions are real; memory-recall metrics have not accumulated yet.
+ kpis = {
+ tokensSaved: null,
+ tokensSource: "none",
+ skillsCreated,
+ memorySearches: 0,
+ sessionsCount,
+ userTokensSaved: null,
+ orgStatsFetchedAt: org ? org.fetchedAt : null,
+ orgStatsStale: org ? org.stale : false,
+ orgStatsOffline: org ? org.offline : false,
+ };
+}
+
+process.stdout.write(
+ JSON.stringify({
+ repoKey,
+ repoProject,
+ generatedAt: new Date().toISOString(),
+ kpis,
+ graph,
+ }),
+);
diff --git a/harnesses/cursor/extension/scripts/load-goals.mjs b/harnesses/cursor/extension/scripts/load-goals.mjs
new file mode 100644
index 00000000..8bfbf2de
--- /dev/null
+++ b/harnesses/cursor/extension/scripts/load-goals.mjs
@@ -0,0 +1,62 @@
+/**
+ * List goals from the Deeplake `hivemind_goals` table.
+ *
+ * Self-contained: reads credentials and queries the Deeplake HTTP endpoint
+ * directly. Mirrors core src/commands/goal.ts goalList, with latest-version
+ * dedup per goal_id (the VFS write path appends a fresh row per overwrite).
+ * Prints a GoalsListResult JSON to stdout.
+ *
+ * argv[2]: filter — "mine" (default) or "all".
+ */
+import { loadCreds, query, sqlIdent, sqlStr, tableNames, isMissingTableError } from "./lib/deeplake.mjs";
+
+const filter = process.argv[2] === "all" ? "all" : "mine";
+
+function emit(obj) {
+ process.stdout.write(JSON.stringify(obj));
+}
+
+const creds = loadCreds();
+if (!creds) {
+ emit({ loggedOut: true, goals: [], message: "Log in with `hivemind login` to track team goals." });
+ process.exit(0);
+}
+
+const table = sqlIdent(tableNames().goals);
+const where = filter === "mine" ? `WHERE owner = '${sqlStr(creds.userName)}'` : "";
+
+let rows;
+try {
+ rows = await query(
+ creds,
+ `SELECT goal_id, owner, status, content, version, created_at FROM "${table}" ${where} ORDER BY version DESC, created_at DESC LIMIT 200`,
+ );
+} catch (e) {
+ if (isMissingTableError(e?.message)) {
+ emit({ loggedOut: false, goals: [] });
+ process.exit(0);
+ }
+ emit({ loggedOut: false, goals: [], message: "Could not load goals." });
+ process.exit(0);
+}
+
+const latest = new Map();
+for (const r of rows) {
+ const goalId = String(r.goal_id ?? "");
+ if (!goalId || latest.has(goalId)) continue;
+ const text = String(r.content ?? "").split(/\r?\n/)[0].trim();
+ latest.set(goalId, {
+ goalId,
+ owner: String(r.owner ?? ""),
+ status: String(r.status ?? ""),
+ text,
+ createdAt: String(r.created_at ?? ""),
+ });
+}
+
+const goals = [...latest.values()]
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt))
+ .slice(0, 50)
+ .map(({ goalId, owner, status, text }) => ({ goalId, owner, status, text }));
+
+emit({ loggedOut: false, goals });
diff --git a/harnesses/cursor/extension/scripts/load-rules.mjs b/harnesses/cursor/extension/scripts/load-rules.mjs
new file mode 100644
index 00000000..4fef11da
--- /dev/null
+++ b/harnesses/cursor/extension/scripts/load-rules.mjs
@@ -0,0 +1,69 @@
+/**
+ * List team rules from the Deeplake `hivemind_rules` table.
+ *
+ * Self-contained: reads credentials and queries the Deeplake HTTP endpoint
+ * directly (see lib/deeplake.mjs). Mirrors the latest-version-per-rule dedup
+ * in core src/rules/read.ts. Prints a RulesListResult JSON to stdout.
+ */
+import { loadCreds, query, sqlIdent, tableNames, isMissingTableError } from "./lib/deeplake.mjs";
+
+const status = process.argv[2] || "active";
+const limit = parseInt(process.argv[3] || "10", 10);
+
+function emit(obj) {
+ process.stdout.write(JSON.stringify(obj));
+}
+
+const creds = loadCreds();
+if (!creds) {
+ emit({ loggedOut: true, rules: [], message: "Log in with `hivemind login` to manage team rules." });
+ process.exit(0);
+}
+
+const table = sqlIdent(tableNames().rules);
+
+let rows;
+try {
+ rows = await query(creds, `SELECT id, rule_id, text, scope, status, assigned_by, version, created_at FROM "${table}" ORDER BY version DESC, created_at DESC, id DESC`);
+} catch (e) {
+ // The rules table is created lazily by the CLI on first write. Until then
+ // a read 400s with "does not exist" — that just means no rules yet.
+ if (isMissingTableError(e?.message)) {
+ emit({ loggedOut: false, rules: [] });
+ process.exit(0);
+ }
+ emit({ loggedOut: false, rules: [], message: "Could not load rules." });
+ process.exit(0);
+}
+
+const latest = new Map();
+for (const r of rows) {
+ const versionRaw = r.version;
+ const version = typeof versionRaw === "number" ? versionRaw : Number(versionRaw);
+ if (!Number.isFinite(version)) continue;
+ const ruleId = String(r.rule_id ?? "");
+ if (!ruleId || latest.has(ruleId)) continue;
+ latest.set(ruleId, {
+ id: String(r.id ?? ""),
+ rule_id: ruleId,
+ text: String(r.text ?? ""),
+ status: String(r.status ?? ""),
+ assigned_by: String(r.assigned_by ?? ""),
+ version,
+ created_at: String(r.created_at ?? ""),
+ });
+}
+
+const filtered = [...latest.values()].filter((r) => (status === "all" ? true : r.status === status));
+filtered.sort((a, b) => b.created_at.localeCompare(a.created_at) || b.id.localeCompare(a.id));
+
+emit({
+ loggedOut: false,
+ rules: filtered.slice(0, limit).map((r) => ({
+ id: r.rule_id,
+ status: r.status,
+ version: r.version,
+ author: r.assigned_by,
+ text: r.text,
+ })),
+});
diff --git a/harnesses/cursor/extension/scripts/load-session-summary.mjs b/harnesses/cursor/extension/scripts/load-session-summary.mjs
new file mode 100644
index 00000000..f37f4df0
--- /dev/null
+++ b/harnesses/cursor/extension/scripts/load-session-summary.mjs
@@ -0,0 +1,91 @@
+/**
+ * Load a session summary: remote Deeplake memory table first, local disk
+ * fallback second.
+ *
+ * Self-contained: reads credentials and queries the Deeplake HTTP endpoint
+ * directly (see lib/deeplake.mjs). Mirrors the resolution order in the core
+ * memory summary path. Prints a SessionSummaryResult JSON to stdout.
+ */
+import { existsSync, readFileSync } from "node:fs";
+import { homedir } from "node:os";
+import { join } from "node:path";
+import { loadCreds, query, sqlIdent, sqlStr, tableNames } from "./lib/deeplake.mjs";
+
+const sessionId = process.argv[2];
+const userArg = process.argv[3] ?? "";
+
+function emit(obj) {
+ process.stdout.write(JSON.stringify(obj));
+}
+
+if (!sessionId || !/^[a-zA-Z0-9_-]{1,128}$/.test(sessionId)) {
+ emit({ text: null, source: "invalid", message: "Invalid session id." });
+ process.exit(0);
+}
+
+function localSummaryPath(userName) {
+ if (!userName || userName.includes("/") || userName.includes("\\") || userName.includes("..")) {
+ return null;
+ }
+ return join(homedir(), ".deeplake", "memory", "summaries", userName, `${sessionId}.md`);
+}
+
+function readLocal(userName) {
+ const path = localSummaryPath(userName);
+ if (!path || !existsSync(path)) return null;
+ try {
+ return readFileSync(path, "utf-8");
+ } catch {
+ return null;
+ }
+}
+
+async function readRemote(creds) {
+ if (!creds || !creds.userName) return { text: null, unreachable: false };
+ let table;
+ try {
+ table = sqlIdent(tableNames().memory);
+ } catch {
+ return { text: null, unreachable: false };
+ }
+ const vpath = `/summaries/${creds.userName}/${sessionId}.md`;
+ try {
+ const rows = await query(
+ creds,
+ `SELECT summary FROM "${table}" WHERE path = '${sqlStr(vpath)}' AND author = '${sqlStr(creds.userName)}' ` +
+ `AND summary <> '' ORDER BY last_update_date DESC LIMIT 1`,
+ 4000,
+ );
+ if (!rows || rows.length === 0) return { text: null, unreachable: false };
+ const summary = rows[0]?.summary;
+ return { text: typeof summary === "string" && summary.trim() ? summary : null, unreachable: false };
+ } catch {
+ return { text: null, unreachable: true };
+ }
+}
+
+const creds = loadCreds();
+const userName = userArg || creds?.userName || "unknown";
+
+const remote = await readRemote(creds);
+if (remote.text) {
+ emit({ text: remote.text, source: "remote", message: null });
+ process.exit(0);
+}
+
+const local = readLocal(userName);
+if (local) {
+ emit({ text: local, source: "local", message: null });
+ process.exit(0);
+}
+
+if (remote.unreachable) {
+ emit({
+ text: null,
+ source: "unreachable",
+ message: "Memory table unreachable. Showing no summary until connectivity returns.",
+ });
+ process.exit(0);
+}
+
+emit({ text: null, source: "missing", message: `No summary found for session ${sessionId}.` });
diff --git a/harnesses/cursor/extension/scripts/load-sessions.mjs b/harnesses/cursor/extension/scripts/load-sessions.mjs
new file mode 100644
index 00000000..0bc9da47
--- /dev/null
+++ b/harnesses/cursor/extension/scripts/load-sessions.mjs
@@ -0,0 +1,65 @@
+/**
+ * List recent captured sessions from the Deeplake `sessions` table.
+ *
+ * Self-contained: reads credentials and queries the Deeplake HTTP endpoint
+ * directly. Mirrors the grouped listing in core src/commands/session-prune.ts
+ * (one row per session path, newest first). Sessions are scoped to the
+ * current repo's project when possible, falling back to all of the user's
+ * recent sessions. Prints RecentSession[] JSON to stdout.
+ */
+import { basename } from "node:path";
+import { loadCreds, query, sqlIdent, sqlStr, tableNames } from "./lib/deeplake.mjs";
+
+const cwd = process.argv[2] || process.cwd();
+
+function emit(arr) {
+ process.stdout.write(JSON.stringify(arr));
+}
+
+/** /sessions//___.jsonl -> sessionId */
+function extractSessionId(path) {
+ const m = String(path).match(/\/sessions\/[^/]+\/[^/]+_([^.]+)\.jsonl$/);
+ if (m) return m[1];
+ return String(path).split("/").pop()?.replace(/\.jsonl$/, "") ?? String(path);
+}
+
+const creds = loadCreds();
+if (!creds || !creds.userName) {
+ emit([]);
+ process.exit(0);
+}
+
+const table = sqlIdent(tableNames().sessions);
+
+let rows;
+try {
+ rows = await query(
+ creds,
+ `SELECT path, COUNT(*) as cnt, MAX(creation_date) as last_event, MAX(project) as project ` +
+ `FROM "${table}" WHERE author = '${sqlStr(creds.userName)}' ` +
+ `GROUP BY path ORDER BY last_event DESC LIMIT 100`,
+ );
+} catch {
+ emit([]);
+ process.exit(0);
+}
+
+const all = rows.map((r) => {
+ const eventCount = Number(r.cnt) || 0;
+ return {
+ sessionId: extractSessionId(r.path),
+ endedAt: String(r.last_event ?? ""),
+ eventCount,
+ memorySearchCount: eventCount,
+ project: r.project ? String(r.project) : null,
+ hadRecall: eventCount > 0,
+ };
+});
+
+// Prefer sessions from this repo's project; fall back to all when the
+// project name does not line up with the captured `project` column.
+const repoProject = basename(cwd);
+const scoped = all.filter((s) => s.project && s.project === repoProject);
+const result = (scoped.length > 0 ? scoped : all).slice(0, 20);
+
+emit(result);
diff --git a/harnesses/cursor/extension/src/auth/api-key.ts b/harnesses/cursor/extension/src/auth/api-key.ts
new file mode 100644
index 00000000..b7a78174
--- /dev/null
+++ b/harnesses/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/harnesses/cursor/extension/src/auth/detector.ts b/harnesses/cursor/extension/src/auth/detector.ts
new file mode 100644
index 00000000..0589a6a9
--- /dev/null
+++ b/harnesses/cursor/extension/src/auth/detector.ts
@@ -0,0 +1,99 @@
+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<"online" | "logged_out" | "offline"> {
+ 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),
+ });
+ if (resp.status === 401 || resp.status === 403) return "logged_out";
+ return resp.ok ? "online" : "offline";
+ } catch {
+ return "offline";
+ }
+}
+
+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 === "offline") {
+ return {
+ state: "unknown_offline",
+ identity,
+ orgName: creds.orgName,
+ workspaceId: creds.workspaceId,
+ cursorAgentLoggedIn,
+ cursorAgentMessage,
+ };
+ }
+ if (online === "logged_out") {
+ return {
+ state: "logged_out",
+ 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/harnesses/cursor/extension/src/auth/device-flow.ts b/harnesses/cursor/extension/src/auth/device-flow.ts
new file mode 100644
index 00000000..0ac2ea24
--- /dev/null
+++ b/harnesses/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/harnesses/cursor/extension/src/auth/index.ts b/harnesses/cursor/extension/src/auth/index.ts
new file mode 100644
index 00000000..719cfb58
--- /dev/null
+++ b/harnesses/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/harnesses/cursor/extension/src/auth/logout.ts b/harnesses/cursor/extension/src/auth/logout.ts
new file mode 100644
index 00000000..ff1d9a68
--- /dev/null
+++ b/harnesses/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/harnesses/cursor/extension/src/auth/safe-url.ts b/harnesses/cursor/extension/src/auth/safe-url.ts
new file mode 100644
index 00000000..1e8da5db
--- /dev/null
+++ b/harnesses/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/harnesses/cursor/extension/src/bridge/auto-sync.ts b/harnesses/cursor/extension/src/bridge/auto-sync.ts
new file mode 100644
index 00000000..0277075c
--- /dev/null
+++ b/harnesses/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/harnesses/cursor/extension/src/bridge/skill-sync.ts b/harnesses/cursor/extension/src/bridge/skill-sync.ts
new file mode 100644
index 00000000..3d4afe3e
--- /dev/null
+++ b/harnesses/cursor/extension/src/bridge/skill-sync.ts
@@ -0,0 +1,361 @@
+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";
+
+function canonicalSkillsRoot(): string {
+ return join(homedir(), ".claude", "skills");
+}
+
+function agentRoots(projectRoot?: string): string[] {
+ // Mirrors src/skillify/agent-roots.ts detectAgentSkillsRoots().
+ const home = homedir();
+ const canonicalRoot = canonicalSkillsRoot();
+ 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"));
+
+ if (codexInstalled || piInstalled) out.push(join(home, ".agents", "skills"));
+ if (hermesInstalled) out.push(join(home, ".hermes", "skills"));
+ 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.filter((p) => p !== canonicalRoot);
+}
+
+function fanOutSymlinks(canonicalDir: string, dirName: string, agentRootsList: string[]): string[] {
+ const out: string[] = [];
+ for (const root of agentRootsList) {
+ 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 fanOutWithConflicts(canonicalDir: string, dirName: string, roots: string[]): { links: string[]; conflicts: string[] } {
+ const links = fanOutSymlinks(canonicalDir, dirName, roots);
+ const conflicts: string[] = [];
+ for (const root of roots) {
+ const link = join(root, dirName);
+ if (links.includes(link)) continue;
+ try {
+ const st = lstatSync(link);
+ if (!st.isSymbolicLink()) conflicts.push(link);
+ } catch {
+ /* permission or missing — not a user-file conflict */
+ }
+ }
+ return { links, conflicts };
+}
+
+function listPulledSkillDirs(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 listMinedSkillNames(projectRoot?: string): string[] {
+ const stateDir = join(homedir(), ".deeplake", "state", "skillify");
+ const names = new Set();
+ if (existsSync(stateDir)) {
+ for (const file of readdirSync(stateDir)) {
+ if (!file.endsWith(".json") || file === "config.json" || file === "pulled.json") continue;
+ try {
+ const state = JSON.parse(readFileSync(join(stateDir, file), "utf-8")) as {
+ skillsGenerated?: string[];
+ project?: string;
+ };
+ if (projectRoot && state.project) {
+ const base = basename(projectRoot);
+ if (state.project !== base && !projectRoot.includes(state.project)) continue;
+ }
+ for (const n of state.skillsGenerated ?? []) {
+ if (typeof n === "string" && n.length > 0) names.add(n);
+ }
+ } catch {
+ /* ignore */
+ }
+ }
+ }
+ if (projectRoot) {
+ const projectSkills = join(projectRoot, ".claude", "skills");
+ if (existsSync(projectSkills)) {
+ for (const name of readdirSync(projectSkills)) {
+ if (name.includes("--")) continue;
+ if (existsSync(join(projectSkills, name, "SKILL.md"))) names.add(name);
+ }
+ }
+ }
+ return [...names].sort();
+}
+
+function readSkillShareScope(skillPath: string): "me" | "team" | "unknown" {
+ try {
+ const text = readFileSync(join(skillPath, "SKILL.md"), "utf-8");
+ const m = text.match(/^scope:\s*(me|team)\s*$/m);
+ if (m) return m[1] as "me" | "team";
+ } catch {
+ /* ignore */
+ }
+ return "unknown";
+}
+
+function parseDirName(dirName: string): { name: string; author: string } {
+ const idx = dirName.lastIndexOf("--");
+ if (idx <= 0) return { name: dirName, author: "" };
+ return { name: dirName.slice(0, idx), author: dirName.slice(idx + 2) };
+}
+
+interface PulledEntry {
+ dirName: string;
+ name: string;
+ author: string;
+ install: "global" | "project";
+ installRoot: string;
+ symlinks: string[];
+}
+
+interface PulledManifest {
+ version: 1;
+ entries: PulledEntry[];
+}
+
+function manifestPath(): string {
+ return join(homedir(), ".deeplake", "state", "skillify", "pulled.json");
+}
+
+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 (parsed.version === 1 && Array.isArray(parsed.entries)) return parsed;
+ } catch {
+ /* ignore */
+ }
+ return { version: 1, entries: [] };
+}
+
+function writeManifest(manifest: PulledManifest): void {
+ const path = manifestPath();
+ mkdirSync(dirname(path), { recursive: true });
+ writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
+}
+
+function mergeSymlinksIntoManifest(
+ install: "global" | "project",
+ installRoot: string,
+ dirName: string,
+ freshLinks: string[],
+): void {
+ if (freshLinks.length === 0) return;
+ const manifest = loadManifest();
+ const existing = manifest.entries.find(
+ (e) => e.install === install && e.installRoot === installRoot && e.dirName === dirName,
+ );
+ const parsed = parseDirName(dirName);
+ const symlinks = [...new Set([...(existing?.symlinks ?? []), ...freshLinks])].sort();
+ const next = {
+ dirName,
+ name: existing?.name ?? parsed.name,
+ author: existing?.author ?? parsed.author,
+ install,
+ installRoot,
+ symlinks,
+ };
+ const idx = manifest.entries.findIndex(
+ (e) => e.install === install && e.installRoot === installRoot && e.dirName === dirName,
+ );
+ if (idx >= 0) manifest.entries[idx] = { ...manifest.entries[idx]!, ...next };
+ else manifest.entries.push(next);
+ writeManifest(manifest);
+}
+
+/** Sync canonical pulled skills into agent skill directories (incl. Cursor). */
+export function syncSkillsToCursor(projectRoot?: string): SkillSyncState {
+ const skillsRoot = canonicalSkillsRoot();
+ const roots = agentRoots(projectRoot);
+ const results: SkillSyncResult[] = [];
+ const dirs = listPulledSkillDirs(skillsRoot);
+
+ if (roots.length === 0) {
+ return {
+ lastSyncAt: new Date().toISOString(),
+ results: dirs.map((dirName) => ({
+ skillName: dirName,
+ status: "skipped",
+ reason: "No agent skill roots detected",
+ })),
+ 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, conflicts } = fanOutWithConflicts(canonicalDir, dirName, roots);
+ if (links.length === 0) {
+ errored++;
+ results.push({
+ skillName: dirName,
+ status: "errored",
+ reason: conflicts.length > 0
+ ? `Blocked by existing file at ${conflicts[0]}`
+ : "Could not create symlinks (permission or filesystem error)",
+ });
+ continue;
+ }
+ if (links.length < roots.length || conflicts.length > 0) {
+ errored++;
+ const conflictNote = conflicts.length > 0 ? `; conflict at ${conflicts.join(", ")}` : "";
+ results.push({
+ skillName: dirName,
+ status: "errored",
+ path: links.join(", "),
+ reason: `Partial reach: ${links.length}/${roots.length} roots${conflictNote}`,
+ });
+ } else {
+ synced++;
+ results.push({
+ skillName: dirName,
+ status: "synced",
+ path: links.join(", "),
+ });
+ }
+ mergeSymlinksIntoManifest("global", skillsRoot, dirName, links);
+ }
+
+ return {
+ lastSyncAt: new Date().toISOString(),
+ results,
+ syncedCount: synced,
+ skippedCount: skipped,
+ erroredCount: errored,
+ };
+}
+
+/** Backfill agent symlinks for skills already recorded in the pull manifest. */
+export function backfillCursorLinks(projectRoot?: string): number {
+ const manifest = loadManifest();
+ const roots = agentRoots(projectRoot);
+ if (roots.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 { links } = fanOutWithConflicts(canonical, entry.dirName, roots);
+ if (links.length === 0) continue;
+ mergeSymlinksIntoManifest(entry.install, entry.installRoot, entry.dirName, links);
+ updated++;
+ }
+ return updated;
+}
+
+/** List locally mined skills for the promoter pane (not pulled --author dirs). */
+export function listLocalSkillsForPromoter(): Array<{
+ dirName: string;
+ scope: "global" | "project";
+ path: string;
+ shareScope: "me" | "team" | "unknown";
+}> {
+ const out: Array<{ dirName: string; scope: "global" | "project"; path: string; shareScope: "me" | "team" | "unknown" }> = [];
+ const workspace = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
+ const globalRoot = canonicalSkillsRoot();
+
+ const seen = new Set();
+ for (const name of listMinedSkillNames(workspace)) {
+ const projectPath = workspace ? join(workspace, ".claude", "skills", name) : "";
+ const globalPath = join(globalRoot, name);
+ if (workspace && existsSync(join(projectPath, "SKILL.md"))) {
+ out.push({
+ dirName: name,
+ scope: "project",
+ path: projectPath,
+ shareScope: readSkillShareScope(projectPath),
+ });
+ seen.add(name);
+ } else if (existsSync(join(globalPath, "SKILL.md"))) {
+ out.push({
+ dirName: name,
+ scope: "global",
+ path: globalPath,
+ shareScope: readSkillShareScope(globalPath),
+ });
+ seen.add(name);
+ }
+ }
+
+ // Reconcile with what the settings "skills synced" count reports: pulled
+ // skills (`--` dirs under the canonical root) are real,
+ // installed skills too. Surfacing them here keeps the Skills tab from
+ // claiming "no skills" while settings shows a non-zero synced count.
+ for (const dirName of listPulledSkillDirs(globalRoot)) {
+ if (seen.has(dirName)) continue;
+ const pulledPath = join(globalRoot, dirName);
+ if (!existsSync(join(pulledPath, "SKILL.md"))) continue;
+ const declared = readSkillShareScope(pulledPath);
+ out.push({
+ dirName,
+ scope: "global",
+ path: pulledPath,
+ shareScope: declared === "unknown" ? "team" : declared,
+ });
+ seen.add(dirName);
+ }
+ return out;
+}
+
+export function skillDirLabel(dirName: string): string {
+ return dirName;
+}
+
+export function basenameSkill(dirName: string): string {
+ return basename(dirName);
+}
diff --git a/harnesses/cursor/extension/src/extension.ts b/harnesses/cursor/extension/src/extension.ts
new file mode 100644
index 00000000..fb2fc870
--- /dev/null
+++ b/harnesses/cursor/extension/src/extension.ts
@@ -0,0 +1,75 @@
+import * as vscode from "vscode";
+import { join } from "node:path";
+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 { setBundledExtensionSrc } from "./health";
+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…");
+ setBundledExtensionSrc(join(context.extensionUri.fsPath, "bundle"));
+
+ poller = new HealthPoller();
+ statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
+ statusBarItem.command = "hivemind.showStatus";
+ statusBarItem.show();
+ context.subscriptions.push(statusBarItem);
+
+ const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
+
+ 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)),
+ vscode.commands.registerCommand("hivemind.pollHealthNow", () => poller!.pollOnce(workspaceRoot)),
+ ...registerHivemindCommands(
+ () => poller!.pollOnce(workspaceRoot),
+ () => {
+ void poller!.pollOnce(workspaceRoot).then((snap) => showStatusDetail(snap));
+ },
+ ),
+ vscode.commands.registerCommand("hivemind.openDashboard", () => {
+ DashboardPanel.createOrShow(context.extensionUri, context);
+ }),
+ );
+
+ registerDashboardWebview(context);
+
+ 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/harnesses/cursor/extension/src/graph/editor-sync.ts b/harnesses/cursor/extension/src/graph/editor-sync.ts
new file mode 100644
index 00000000..74f3bc60
--- /dev/null
+++ b/harnesses/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/harnesses/cursor/extension/src/graph/impact-overlay.ts b/harnesses/cursor/extension/src/graph/impact-overlay.ts
new file mode 100644
index 00000000..53de7d9e
--- /dev/null
+++ b/harnesses/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/harnesses/cursor/extension/src/graph/snapshot-loader.ts b/harnesses/cursor/extension/src/graph/snapshot-loader.ts
new file mode 100644
index 00000000..366b2240
--- /dev/null
+++ b/harnesses/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/harnesses/cursor/extension/src/graph/types.ts b/harnesses/cursor/extension/src/graph/types.ts
new file mode 100644
index 00000000..8a0c29e0
--- /dev/null
+++ b/harnesses/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/harnesses/cursor/extension/src/health/checker.ts b/harnesses/cursor/extension/src/health/checker.ts
new file mode 100644
index 00000000..9285196f
--- /dev/null
+++ b/harnesses/cursor/extension/src/health/checker.ts
@@ -0,0 +1,305 @@
+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,
+ cursorPluginDir,
+ 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(cursorPluginDir(), ".hivemind_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 execErr = err as { status?: number; code?: string; message?: string };
+ if (execErr.code === "ENOENT") {
+ return {
+ id: "d3",
+ label: "cursor-agent login",
+ status: "missing",
+ message: "cursor-agent binary disappeared between detection and status check.",
+ };
+ }
+ const exitCode = typeof execErr.status === "number" ? execErr.status : undefined;
+ const msg = err instanceof Error ? err.message : String(err);
+ const loggedOut =
+ exitCode === 401 ||
+ exitCode === 403 ||
+ /not logged/i.test(msg) ||
+ /login required/i.test(msg);
+ 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 provisionedPresent = bundlePresent;
+
+ const d1 = checkHivemindCli();
+ const d2 = checkCursorAgentCli();
+ const d3 = checkCursorAgentLogin();
+ const { dimension: d4, wiredVersion } = checkHooksWired(bundlePresent ? bundleVersion : undefined);
+
+ if (!provisionedPresent && !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") && provisionedPresent;
+
+ return {
+ checkedAt: new Date().toISOString(),
+ dimensions,
+ bundlePresent: provisionedPresent,
+ bundleVersion,
+ wiredVersion,
+ allHealthy,
+ summariesDisabled,
+ };
+}
+
+export function getHivemindInstallCommand(): string {
+ return "npm install -g @deeplake/hivemind";
+}
diff --git a/harnesses/cursor/extension/src/health/index.ts b/harnesses/cursor/extension/src/health/index.ts
new file mode 100644
index 00000000..46282c98
--- /dev/null
+++ b/harnesses/cursor/extension/src/health/index.ts
@@ -0,0 +1,3 @@
+export { runHealthCheck, getHivemindInstallCommand } from "./checker";
+export { autoWireHooks, unwireHooks, setBundledExtensionSrc } from "./wirings";
+export type { WireResult } from "./wirings";
diff --git a/harnesses/cursor/extension/src/health/wirings.ts b/harnesses/cursor/extension/src/health/wirings.ts
new file mode 100644
index 00000000..3f4368e2
--- /dev/null
+++ b/harnesses/cursor/extension/src/health/wirings.ts
@@ -0,0 +1,168 @@
+import { cpSync, existsSync, mkdirSync, readFileSync, statSync, 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