From d2bf37e19d18e853aa9894fc043f8b225104e04c Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Mon, 13 Jul 2026 16:32:43 +0800 Subject: [PATCH 01/11] feat(server): add MCP server modules and config route Implement a read-only MCP server (Streamable HTTP at /mcp + SSE fallback at /mcp/sse) mounted only when enabled and a token is configured. Adds the mcp_configs table, GET/PUT /api/configs/mcp (token encrypted like vector search), SqliteMcpProvider, tool registration (conditional semantic search), and the Express mount. Token comparison uses constant-time timingSafeEqual. --- server/package-lock.json | 641 ++++++++++++++++++++++++++++++- server/package.json | 16 +- server/src/db/schema.ts | 9 + server/src/index.ts | 4 + server/src/mcp/index.ts | 132 +++++++ server/src/mcp/server.ts | 15 + server/src/mcp/sqliteProvider.ts | 331 ++++++++++++++++ server/src/mcp/tools.ts | 141 +++++++ server/src/mcp/types.ts | 75 ++++ server/src/routes/configs.ts | 68 ++++ 10 files changed, 1409 insertions(+), 23 deletions(-) create mode 100644 server/src/mcp/index.ts create mode 100644 server/src/mcp/server.ts create mode 100644 server/src/mcp/sqliteProvider.ts create mode 100644 server/src/mcp/tools.ts create mode 100644 server/src/mcp/types.ts diff --git a/server/package-lock.json b/server/package-lock.json index 5658d03e..5a4fcd2f 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -8,13 +8,15 @@ "name": "github-stars-manager-server", "version": "0.1.0", "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", "axios": "^1.7.0", "better-sqlite3": "^11.0.0", "cors": "^2.8.5", "express": "^4.21.0", "helmet": "^7.1.0", "morgan": "^1.10.0", - "socks-proxy-agent": "^9.0.0" + "socks-proxy-agent": "^9.0.0", + "zod": "^3.25.76" }, "devDependencies": { "@types/better-sqlite3": "^7.6.8", @@ -470,6 +472,18 @@ "node": ">=18" } }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -490,6 +504,392 @@ "dev": true, "license": "MIT" }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -1211,6 +1611,39 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "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==", + "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": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", @@ -1561,7 +1994,6 @@ "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", @@ -1826,6 +2258,27 @@ "node": ">= 0.6" } }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -1905,6 +2358,30 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "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==", + "license": "MIT" + }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -1912,6 +2389,22 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -2175,6 +2668,15 @@ "node": ">=16.0.0" } }, + "node_modules/hono": { + "version": "4.12.30", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.30.tgz", + "integrity": "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -2303,6 +2805,12 @@ "node": ">= 0.10" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", @@ -2320,9 +2828,17 @@ "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/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -2330,6 +2846,18 @@ "dev": true, "license": "MIT" }, + "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==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/local-pkg": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", @@ -2708,7 +3236,6 @@ "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" @@ -2744,6 +3271,15 @@ "dev": true, "license": "ISC" }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-types": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", @@ -2941,6 +3477,15 @@ "node": ">= 6" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -2996,6 +3541,55 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -3089,7 +3683,6 @@ "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" @@ -3102,21 +3695,20 @@ "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/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -3128,13 +3720,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -4281,7 +4873,6 @@ "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" @@ -4328,6 +4919,24 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } } } } diff --git a/server/package.json b/server/package.json index 652bf88c..3bd099ed 100644 --- a/server/package.json +++ b/server/package.json @@ -11,23 +11,25 @@ "test:watch": "vitest" }, "dependencies": { - "express": "^4.21.0", + "@modelcontextprotocol/sdk": "^1.29.0", + "axios": "^1.7.0", "better-sqlite3": "^11.0.0", "cors": "^2.8.5", + "express": "^4.21.0", "helmet": "^7.1.0", "morgan": "^1.10.0", - "axios": "^1.7.0", - "socks-proxy-agent": "^9.0.0" + "socks-proxy-agent": "^9.0.0", + "zod": "^3.25.76" }, "devDependencies": { - "@types/express": "^4.17.21", "@types/better-sqlite3": "^7.6.8", "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", "@types/morgan": "^1.9.9", + "@types/supertest": "^6.0.2", + "supertest": "^6.3.4", "tsx": "^4.7.0", "typescript": "^5.5.3", - "vitest": "^1.6.0", - "supertest": "^6.3.4", - "@types/supertest": "^6.0.2" + "vitest": "^1.6.0" } } diff --git a/server/src/db/schema.ts b/server/src/db/schema.ts index e45b5931..ffb237cf 100644 --- a/server/src/db/schema.ts +++ b/server/src/db/schema.ts @@ -134,6 +134,15 @@ export function initializeSchema(db: Database.Database): void { created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); + + CREATE TABLE IF NOT EXISTS mcp_configs ( + id TEXT PRIMARY KEY DEFAULT 'default', + enabled INTEGER NOT NULL DEFAULT 0, + port INTEGER NOT NULL DEFAULT 0, + token_encrypted TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) + ); `); addColumnIfMissing(db, 'ai_configs', 'reasoning_effort', 'TEXT'); diff --git a/server/src/index.ts b/server/src/index.ts index d7ef9e5c..78fa77e3 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -16,6 +16,7 @@ import configsRouter from './routes/configs.js'; import syncRouter from './routes/sync.js'; import proxyRouter from './routes/proxy.js'; import logsRouter from './routes/logs.js'; +import { initMcp } from './mcp/index.js'; export function createApp(): express.Express { const app = express(); @@ -45,6 +46,9 @@ export function createApp(): express.Express { // Wave 4: Logs route app.use(logsRouter); + // MCP server (Streamable HTTP at /mcp + SSE fallback at /mcp/sse), mounted only when enabled + initMcp(app); + // Global error handler app.use(errorHandler); diff --git a/server/src/mcp/index.ts b/server/src/mcp/index.ts new file mode 100644 index 00000000..8a12fd47 --- /dev/null +++ b/server/src/mcp/index.ts @@ -0,0 +1,132 @@ +import crypto from 'node:crypto'; +import type { Express, Request, Response } from 'express'; +import { getDb } from '../db/connection.js'; +import { decrypt } from '../services/crypto.js'; +import { config } from '../config.js'; +import { logger } from '../services/logger.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import { createMcpServer } from './server.js'; +import { SqliteMcpProvider } from './sqliteProvider.js'; + +function extractBearer(req: Request): string { + const header = req.headers['authorization'] ?? ''; + if (typeof header === 'string' && header.toLowerCase().startsWith('bearer ')) { + return header.slice(7).trim(); + } + return ''; +} + +// Constant-time comparison to avoid timing attacks on the MCP bearer token +function tokenMatches(provided: string, expected: string): boolean { + const a = Buffer.from(provided); + const b = Buffer.from(expected); + if (a.length !== b.length) return false; + return crypto.timingSafeEqual(a, b); +} + +/** + * Mount the MCP server (Streamable HTTP at /mcp, SSE fallback at /mcp/sse) onto the + * Express app. No-op unless MCP is enabled AND a token is configured — existing routes + * and /api auth are untouched. + */ +export function initMcp(app: Express): void { + try { + const db = getDb(); + const row = db.prepare('SELECT * FROM mcp_configs WHERE id = ?').get('default') as + | Record + | undefined; + if (!row || !row.enabled) { + logger.info('mcp.init', 'MCP server disabled'); + return; + } + + let token = ''; + if (row.token_encrypted) { + try { + token = decrypt(row.token_encrypted as string, config.encryptionKey); + } catch { + token = ''; + } + } + if (!token) { + logger.warn('mcp.init', 'MCP enabled but no token configured; refusing to expose endpoint'); + return; + } + + const vsRow = db + .prepare('SELECT * FROM vector_search_configs WHERE id = ?') + .get('default') as Record | undefined; + const vectorEnabled = !!( + vsRow && + vsRow.enabled && + vsRow.worker_url && + vsRow.embedding_config_id + ); + + const provider = new SqliteMcpProvider(db); + const server = createMcpServer(provider, vectorEnabled); + const checkToken = (req: Request, res: Response): boolean => { + if (!tokenMatches(extractBearer(req), token)) { + res.status(401).json({ error: 'unauthorized' }); + return false; + } + return true; + }; + + // ── Streamable HTTP (primary, MCP 2025 spec) ── + app.all('/mcp', async (req, res) => { + if (!checkToken(req, res)) return; + try { + const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); + res.on('close', () => { + transport.close().catch(() => undefined); + }); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + } catch (err) { + logger.errorFromError('mcp.streamable', 'Streamable HTTP error', err as Error); + if (!res.headersSent) res.status(500).json({ error: 'internal error' }); + } + }); + + // ── SSE fallback (for clients not yet supporting Streamable HTTP) ── + const sseTransports = new Map(); + + app.get('/mcp/sse', async (req, res) => { + if (!checkToken(req, res)) return; + try { + const transport = new SSEServerTransport('/mcp/sse/messages', res); + sseTransports.set(transport.sessionId, transport); + res.on('close', () => { + sseTransports.delete(transport.sessionId); + }); + await server.connect(transport); + await transport.start(); + } catch (err) { + logger.errorFromError('mcp.sse', 'SSE connection error', err as Error); + if (!res.headersSent) res.status(500).json({ error: 'internal error' }); + } + }); + + app.post('/mcp/sse/messages', async (req, res) => { + if (!checkToken(req, res)) return; + const sessionId = req.query.sessionId as string | undefined; + const transport = sessionId ? sseTransports.get(sessionId) : undefined; + if (!transport) { + res.status(404).json({ error: 'unknown session' }); + return; + } + try { + await transport.handlePostMessage(req, res, req.body); + } catch (err) { + logger.errorFromError('mcp.sseMsg', 'SSE message error', err as Error); + if (!res.headersSent) res.status(500).json({ error: 'internal error' }); + } + }); + + logger.info('mcp.init', `MCP server mounted (vectorSearch=${vectorEnabled})`); + } catch (err) { + logger.errorFromError('mcp.init', 'Failed to init MCP server', err as Error); + } +} diff --git a/server/src/mcp/server.ts b/server/src/mcp/server.ts new file mode 100644 index 00000000..5aa7e029 --- /dev/null +++ b/server/src/mcp/server.ts @@ -0,0 +1,15 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { registerMcpTools } from './tools.js'; +import type { McpDataProvider } from './types.js'; + +export const MCP_SERVER_NAME = 'github-stars-manager'; +export const MCP_SERVER_VERSION = '0.7.0'; + +export function createMcpServer(provider: McpDataProvider, vectorEnabled: boolean): McpServer { + const server = new McpServer( + { name: MCP_SERVER_NAME, version: MCP_SERVER_VERSION }, + { capabilities: { tools: {} } } + ); + registerMcpTools(server, provider, vectorEnabled); + return server; +} diff --git a/server/src/mcp/sqliteProvider.ts b/server/src/mcp/sqliteProvider.ts new file mode 100644 index 00000000..09212b8e --- /dev/null +++ b/server/src/mcp/sqliteProvider.ts @@ -0,0 +1,331 @@ +import type Database from 'better-sqlite3'; +import axios from 'axios'; +import { decrypt } from '../services/crypto.js'; +import { config } from '../config.js'; +import type { + McpDataProvider, + McpListFilter, + McpRepoSummary, + McpRepoDetail, + McpRepoComment, + McpReleaseSummary, + McpStat, +} from './types.js'; + +function parseJsonArray(value: unknown): string[] { + if (!value || typeof value !== 'string') return []; + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed.map((v) => String(v)) : []; + } catch { + return []; + } +} + +function mapSummary(row: Record): McpRepoSummary { + return { + fullName: String(row.full_name ?? ''), + name: String(row.name ?? ''), + description: (row.description as string) ?? null, + htmlUrl: String(row.html_url ?? ''), + stars: Number(row.stargazers_count ?? 0), + language: (row.language as string) ?? null, + topics: parseJsonArray(row.topics), + customTags: parseJsonArray(row.custom_tags), + aiTags: parseJsonArray(row.ai_tags), + platforms: parseJsonArray(row.ai_platforms), + category: (row.custom_category as string) ?? null, + updatedAt: (row.updated_at as string) ?? null, + analyzed: !!row.analyzed_at, + }; +} + +export class SqliteMcpProvider implements McpDataProvider { + constructor(private readonly db: Database.Database) {} + + private readVectorConfig(): { enabled: boolean; workerUrl: string; authToken: string; embeddingConfigId: string } | null { + const row = this.db + .prepare('SELECT * FROM vector_search_configs WHERE id = ?') + .get('default') as Record | undefined; + if (!row) return null; + let authToken = ''; + if (row.auth_token_encrypted) { + try { + authToken = decrypt(row.auth_token_encrypted as string, config.encryptionKey); + } catch { + authToken = ''; + } + } + return { + enabled: !!row.enabled, + workerUrl: (row.worker_url as string) ?? '', + authToken, + embeddingConfigId: (row.embedding_config_id as string) ?? '', + }; + } + + async isVectorSearchEnabled(): Promise { + const v = this.readVectorConfig(); + return !!v && v.enabled && !!v.workerUrl && !!v.embeddingConfigId; + } + + listRepositories(filter: McpListFilter): Promise { + const where: string[] = []; + const params: unknown[] = []; + + if (filter.query && filter.query.trim()) { + const q = `%${filter.query.trim()}%`; + where.push( + '(full_name LIKE ? OR description LIKE ? OR topics LIKE ? OR custom_tags LIKE ? OR custom_description LIKE ? OR ai_summary LIKE ?)' + ); + params.push(q, q, q, q, q, q); + } + if (filter.languages && filter.languages.length) { + where.push(`language IN (${filter.languages.map(() => '?').join(',')})`); + params.push(...filter.languages); + } + if (filter.categories && filter.categories.length) { + where.push(`custom_category IN (${filter.categories.map(() => '?').join(',')})`); + params.push(...filter.categories); + } + if (typeof filter.minStars === 'number') { + where.push('stargazers_count >= ?'); + params.push(filter.minStars); + } + if (typeof filter.maxStars === 'number') { + where.push('stargazers_count <= ?'); + params.push(filter.maxStars); + } + if (filter.isAnalyzed !== undefined) { + where.push(filter.isAnalyzed ? 'analyzed_at IS NOT NULL' : 'analyzed_at IS NULL'); + } + + const sql = ` + SELECT * FROM repositories + ${where.length ? `WHERE ${where.join(' AND ')}` : ''} + ORDER BY ${ + filter.sortBy === 'name' + ? 'full_name' + : filter.sortBy === 'starred' + ? 'starred_at' + : filter.sortBy === 'updated' + ? 'updated_at' + : 'stargazers_count' + } ${filter.sortOrder === 'asc' ? 'ASC' : 'DESC'} + LIMIT ? OFFSET ? + `; + const limit = Math.min(Math.max(filter.limit ?? 50, 1), 200); + const offset = Math.max(filter.offset ?? 0, 0); + params.push(limit, offset); + + const rows = this.db.prepare(sql).all(...params) as Record[]; + let results = rows.map(mapSummary); + + if (filter.tags && filter.tags.length) { + const tags = filter.tags.map((t) => t.toLowerCase()); + results = results.filter((r) => + [...r.customTags, ...r.aiTags, ...r.topics].some((t) => tags.includes(t.toLowerCase())) + ); + } + return Promise.resolve(results); + } + + getRepository(fullName: string): Promise { + const row = this.db + .prepare('SELECT * FROM repositories WHERE full_name = ?') + .get(fullName) as Record | undefined; + if (!row) return Promise.resolve(null); + const summary = mapSummary(row); + return Promise.resolve({ + ...summary, + customDescription: (row.custom_description as string) ?? null, + aiSummary: (row.ai_summary as string) ?? null, + notes: (row.custom_description as string) ?? null, + readmeSnippet: null, + starredAt: (row.starred_at as string) ?? null, + vectorIndexedAt: null, + }); + } + + getRepositoryComments(fullName: string): Promise { + const row = this.db + .prepare('SELECT full_name, custom_description FROM repositories WHERE full_name = ?') + .get(fullName) as Record | undefined; + if (!row) return Promise.resolve(null); + return Promise.resolve({ + fullName: String(row.full_name ?? ''), + customDescription: (row.custom_description as string) ?? null, + notes: (row.custom_description as string) ?? null, + }); + } + + async semanticSearch(query: string, topK: number): Promise> { + const v = this.readVectorConfig(); + if (!v || !v.enabled || !v.workerUrl || !v.embeddingConfigId) { + return []; + } + const embRow = this.db + .prepare('SELECT * FROM embedding_configs WHERE id = ?') + .get(v.embeddingConfigId) as Record | undefined; + if (!embRow) return []; + + const apiKey = embRow.api_key_encrypted ? decrypt(embRow.api_key_encrypted as string, config.encryptionKey) : ''; + const baseUrl = (embRow.base_url as string) ?? ''; + const model = (embRow.model as string) ?? ''; + const apiType = (embRow.api_type as string) ?? 'openai'; + + const vector = await this.embedQuery(apiType, baseUrl, apiKey, model, query); + if (!vector) return []; + + try { + const res = await axios.post( + `${v.workerUrl.replace(/\/+$/, '')}/query`, + { vector, topK: Math.min(topK, 50), threshold: 0.3 }, + { headers: { Authorization: `Bearer ${v.authToken}`, 'Content-Type': 'application/json' }, timeout: 15000 } + ); + const matches = (res.data?.matches ?? []) as Array<{ id?: string; metadata?: { full_name?: string }; score?: number }>; + return matches + .map((m) => ({ + fullName: (m.metadata?.full_name ?? m.id ?? '') as string, + score: typeof m.score === 'number' ? m.score : 0, + })) + .filter((m) => !!m.fullName); + } catch { + return []; + } + } + + private async embedQuery( + apiType: string, + baseUrl: string, + apiKey: string, + model: string, + text: string + ): Promise { + try { + if (apiType === 'ollama') { + const res = await axios.post( + `${baseUrl.replace(/\/+$/, '')}/api/embed`, + { model, input: text }, + { headers: { 'Content-Type': 'application/json' }, timeout: 15000 } + ); + return (res.data?.embeddings?.[0] as number[]) ?? null; + } + if (apiType === 'gemini') { + const res = await axios.post( + `${baseUrl.replace(/\/+$/, '')}/v1beta/models/${model}:batchEmbedContents?key=${apiKey}`, + { requests: [{ model: `models/${model}`, content: { parts: [{ text }] } }] }, + { headers: { 'Content-Type': 'application/json' }, timeout: 15000 } + ); + return (res.data?.embeddings?.[0]?.values as number[]) ?? null; + } + // openai / openai-compatible / siliconflow / cohere + const url = + apiType === 'cohere' + ? `${baseUrl.replace(/\/+$/, '')}/v1/embed` + : `${baseUrl.replace(/\/+$/, '')}/v1/embeddings`; + const res = await axios.post( + url, + apiType === 'cohere' ? { model, texts: [text], input_type: 'search_query' } : { input: [text], model }, + { headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, timeout: 15000 } + ); + if (apiType === 'cohere') return (res.data?.embeddings?.[0]?.values as number[]) ?? null; + return (res.data?.data?.[0]?.embedding as number[]) ?? null; + } catch { + return null; + } + } + + listCategories(): Promise> { + const predefined = this.db.prepare('SELECT id, name FROM categories').all() as Array<{ id: string; name: string }>; + const counts = this.db + .prepare("SELECT custom_category, COUNT(*) AS c FROM repositories WHERE custom_category IS NOT NULL AND custom_category != '' GROUP BY custom_category") + .all() as Array<{ custom_category: string; c: number }>; + const countMap = new Map(counts.map((c) => [c.custom_category, c.c])); + const result = predefined.map((p) => ({ id: p.id, name: p.name, count: countMap.get(p.name) ?? 0 })); + for (const c of counts) { + if (!predefined.some((p) => p.name === c.custom_category)) { + result.push({ id: c.custom_category, name: c.custom_category, count: c.c }); + } + } + return Promise.resolve(result); + } + + listTags(): Promise> { + const rows = this.db + .prepare('SELECT custom_tags, ai_tags, topics FROM repositories') + .all() as Array<{ custom_tags: string; ai_tags: string; topics: string }>; + const tally = new Map(); + for (const row of rows) { + for (const tag of [...parseJsonArray(row.custom_tags), ...parseJsonArray(row.ai_tags), ...parseJsonArray(row.topics)]) { + if (!tag) continue; + tally.set(tag, (tally.get(tag) ?? 0) + 1); + } + } + const result = [...tally.entries()] + .map(([tag, count]) => ({ tag, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 100); + return Promise.resolve(result); + } + + listReleases(since: string | null, limit: number): Promise { + const sql = ` + SELECT repo_full_name, repo_name, tag_name, name, published_at, html_url, prerelease, is_read + FROM releases + ${since ? 'WHERE published_at >= ?' : ''} + ORDER BY published_at DESC + LIMIT ? + `; + const params = since ? [since, Math.min(Math.max(limit, 1), 200)] : [Math.min(Math.max(limit, 1), 200)]; + const rows = this.db.prepare(sql).all(...params) as Record[]; + return Promise.resolve( + rows.map((r) => ({ + repoFullName: String(r.repo_full_name ?? ''), + repoName: String(r.repo_name ?? ''), + tagName: String(r.tag_name ?? ''), + name: (r.name as string) ?? null, + publishedAt: (r.published_at as string) ?? null, + htmlUrl: String(r.html_url ?? ''), + prerelease: !!r.prerelease, + isRead: !!r.is_read, + })) + ); + } + + getStats(): Promise { + const total = (this.db.prepare('SELECT COUNT(*) AS c FROM repositories').get() as { c: number }).c; + const analyzed = ( + this.db.prepare("SELECT COUNT(*) AS c FROM repositories WHERE analyzed_at IS NOT NULL").get() as { c: number } + ).c; + + const langs = this.db + .prepare("SELECT language, COUNT(*) AS c FROM repositories WHERE language IS NOT NULL GROUP BY language") + .all() as Array<{ language: string; c: number }>; + const byLanguage: Record = {}; + for (const l of langs) byLanguage[l.language] = l.c; + + const cats = this.db + .prepare("SELECT custom_category, COUNT(*) AS c FROM repositories WHERE custom_category IS NOT NULL AND custom_category != '' GROUP BY custom_category") + .all() as Array<{ custom_category: string; c: number }>; + const byCategory: Record = {}; + for (const c of cats) byCategory[c.custom_category] = c.c; + + const tags = this.db + .prepare('SELECT custom_tags, ai_tags, topics FROM repositories') + .all() as Array<{ custom_tags: string; ai_tags: string; topics: string }>; + const tally = new Map(); + for (const row of tags) { + for (const tag of [...parseJsonArray(row.custom_tags), ...parseJsonArray(row.ai_tags), ...parseJsonArray(row.topics)]) { + if (!tag) continue; + tally.set(tag, (tally.get(tag) ?? 0) + 1); + } + } + const topTags = [...tally.entries()] + .map(([tag, count]) => ({ tag, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 20); + + return Promise.resolve({ totalRepositories: total, analyzedCount: analyzed, byLanguage, byCategory, topTags }); + } +} diff --git a/server/src/mcp/tools.ts b/server/src/mcp/tools.ts new file mode 100644 index 00000000..648744d0 --- /dev/null +++ b/server/src/mcp/tools.ts @@ -0,0 +1,141 @@ +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { McpDataProvider } from './types.js'; + +function json(content: unknown) { + return { + content: [{ type: 'text' as const, text: typeof content === 'string' ? content : JSON.stringify(content, null, 2) }], + }; +} + +export function registerMcpTools(server: McpServer, provider: McpDataProvider, vectorEnabled: boolean): void { + server.registerTool( + 'search_repositories', + { + description: + 'Search the user\'s starred GitHub repositories stored and curated by GitHub Stars Manager. Supports keyword query plus filters by tags, languages, categories, star range and AI-analyzed status. Returns a list of repository summaries.', + inputSchema: { + query: z.string().optional().describe('Free-text query matched against name, description, topics, tags and notes'), + tags: z.array(z.string()).optional().describe('Filter by any of these tags (custom tags, AI tags or topics)'), + languages: z.array(z.string()).optional().describe('Filter by programming language'), + categories: z.array(z.string()).optional().describe('Filter by custom category name'), + minStars: z.number().optional().describe('Minimum star count'), + maxStars: z.number().optional().describe('Maximum star count'), + isAnalyzed: z.boolean().optional().describe('Only include AI-analyzed repositories when true'), + sortBy: z.enum(['stars', 'updated', 'name', 'starred']).optional(), + sortOrder: z.enum(['desc', 'asc']).optional(), + limit: z.number().optional().describe('Max results (1-200, default 50)'), + }, + }, + async (args) => { + const repos = await provider.listRepositories({ + query: args.query, + tags: args.tags, + languages: args.languages, + categories: args.categories, + minStars: args.minStars, + maxStars: args.maxStars, + isAnalyzed: args.isAnalyzed, + sortBy: args.sortBy, + sortOrder: args.sortOrder, + limit: args.limit, + }); + return json(repos); + } + ); + + server.registerTool( + 'get_repository', + { + description: + 'Get the full detail of a single starred repository by its full_name (owner/repo), including description, AI summary, custom tags, platforms, category and notes.', + inputSchema: { fullName: z.string().describe('Repository full_name, e.g. "owner/repo"') }, + }, + async (args) => { + const repo = await provider.getRepository(args.fullName); + if (!repo) return json({ error: `Repository not found: ${args.fullName}` }); + return json(repo); + } + ); + + server.registerTool( + 'get_repository_comments', + { + description: + 'Get the user-authored notes / custom description / comments attached to a starred repository by full_name. Useful when the user wants to recall what they wrote about a repo.', + inputSchema: { fullName: z.string().describe('Repository full_name, e.g. "owner/repo"') }, + }, + async (args) => { + const comments = await provider.getRepositoryComments(args.fullName); + if (!comments) return json({ error: `Repository not found: ${args.fullName}` }); + return json(comments); + } + ); + + server.registerTool( + 'list_categories', + { + description: 'List all categories (predefined and custom) with the number of starred repositories in each.', + inputSchema: {}, + }, + async () => { + return json(await provider.listCategories()); + } + ); + + server.registerTool( + 'list_tags', + { + description: 'List the most used tags (custom tags, AI tags and topics) across starred repositories with counts.', + inputSchema: {}, + }, + async () => { + return json(await provider.listTags()); + } + ); + + server.registerTool( + 'list_releases', + { + description: + 'List recent releases of starred repositories, optionally filtered by publish date (ISO string, inclusive).', + inputSchema: { + since: z.string().optional().describe('Only releases published at or after this ISO datetime'), + limit: z.number().optional().describe('Max results (1-200, default 50)'), + }, + }, + async (args) => { + return json(await provider.listReleases(args.since ?? null, args.limit ?? 50)); + } + ); + + server.registerTool( + 'get_stats', + { + description: + 'Get aggregate statistics of the user\'s starred repositories: total count, analyzed count, breakdown by language and category, and top tags.', + inputSchema: {}, + }, + async () => { + return json(await provider.getStats()); + } + ); + + if (vectorEnabled) { + server.registerTool( + 'semantic_search_repositories', + { + description: + 'Semantic / natural-language search over starred repositories using the configured vector index. Returns repositories ranked by embedding similarity to the query.', + inputSchema: { + query: z.string().describe('Natural-language query, e.g. "a lightweight Rust CLI for PDF merging"'), + topK: z.number().optional().describe('Number of results (default 10)'), + }, + }, + async (args) => { + const results = await provider.semanticSearch(args.query, args.topK ?? 10); + return json(results); + } + ); + } +} diff --git a/server/src/mcp/types.ts b/server/src/mcp/types.ts new file mode 100644 index 00000000..38467f95 --- /dev/null +++ b/server/src/mcp/types.ts @@ -0,0 +1,75 @@ +export interface McpRepoSummary { + fullName: string; + name: string; + description: string | null; + htmlUrl: string; + stars: number; + language: string | null; + topics: string[]; + customTags: string[]; + aiTags: string[]; + platforms: string[]; + category: string | null; + updatedAt: string | null; + analyzed: boolean; +} + +export interface McpRepoComment { + fullName: string; + customDescription: string | null; + notes: string | null; +} + +export interface McpRepoDetail extends McpRepoSummary { + customDescription: string | null; + aiSummary: string | null; + notes: string | null; + readmeSnippet: string | null; + starredAt: string | null; + vectorIndexedAt: string | null; +} + +export interface McpReleaseSummary { + repoFullName: string; + repoName: string; + tagName: string; + name: string | null; + publishedAt: string | null; + htmlUrl: string; + prerelease: boolean; + isRead: boolean; +} + +export interface McpStat { + totalRepositories: number; + analyzedCount: number; + byLanguage: Record; + byCategory: Record; + topTags: Array<{ tag: string; count: number }>; +} + +export interface McpListFilter { + query?: string; + tags?: string[]; + languages?: string[]; + categories?: string[]; + minStars?: number; + maxStars?: number; + isAnalyzed?: boolean; + sortBy?: 'stars' | 'updated' | 'name' | 'starred'; + sortOrder?: 'desc' | 'asc'; + limit?: number; + offset?: number; +} + +export interface McpDataProvider { + listRepositories(filter: McpListFilter): Promise; + getRepository(fullName: string): Promise; + getRepositoryComments(fullName: string): Promise; + semanticSearch(query: string, topK: number): Promise>; + listCategories(): Promise>; + listTags(): Promise>; + listReleases(since: string | null, limit: number): Promise; + getStats(): Promise; + isVectorSearchEnabled(): Promise; +} diff --git a/server/src/routes/configs.ts b/server/src/routes/configs.ts index 22ecb417..0dfa7496 100644 --- a/server/src/routes/configs.ts +++ b/server/src/routes/configs.ts @@ -754,4 +754,72 @@ router.put('/api/configs/vector-search', (req, res) => { } }); +// GET /api/configs/mcp +router.get('/api/configs/mcp', (req, res) => { + try { + const db = getDb(); + const shouldDecrypt = req.query.decrypt === 'true'; + const row = db.prepare('SELECT * FROM mcp_configs WHERE id = ?').get('default') as Record | undefined; + + if (!row) { + res.json({ enabled: false, port: 0, token: '', tokenStatus: 'empty' }); + return; + } + + let token = ''; + let tokenStatus: SecretStatus = 'empty'; + if (row.token_encrypted) { + const result = getMaskedSecretResult({ + encryptedValue: row.token_encrypted, + encryptionKey: config.encryptionKey, + kind: 'AI API key', + }); + token = shouldDecrypt ? result.decryptedValue : maskApiKey(result.decryptedValue); + tokenStatus = result.status; + } + + res.json({ + enabled: !!row.enabled, + port: typeof row.port === 'number' ? row.port : 0, + token, + tokenStatus, + }); + } catch (err) { + logger.errorFromError('configs.getMcp', 'GET /api/configs/mcp error', err); + res.status(500).json({ error: 'Failed to fetch MCP config', code: 'FETCH_MCP_CONFIG_FAILED' }); + } +}); + +// PUT /api/configs/mcp +router.put('/api/configs/mcp', (req, res) => { + try { + const db = getDb(); + const { enabled, port, token } = req.body as Record; + + let encryptedToken = ''; + const hasToken = Object.prototype.hasOwnProperty.call(req.body, 'token'); + if (hasToken && token === '') { + encryptedToken = ''; + } else if (token && typeof token === 'string' && !token.startsWith('***')) { + encryptedToken = encrypt(token, config.encryptionKey); + } else { + const existing = db.prepare('SELECT token_encrypted FROM mcp_configs WHERE id = ?').get('default') as Record | undefined; + encryptedToken = (existing?.token_encrypted as string) ?? ''; + } + + const isEnabled = enabled === true || enabled === 1; + const mcpPort = typeof port === 'number' && port > 0 ? port : 0; + + db.prepare(` + INSERT OR REPLACE INTO mcp_configs (id, enabled, port, token_encrypted, updated_at) + VALUES (?, ?, ?, ?, datetime('now')) + `).run('default', isEnabled ? 1 : 0, mcpPort, encryptedToken); + + res.json({ updated: true }); + } catch (err) { + logger.errorFromError('configs.updateMcp', 'PUT /api/configs/mcp error', err); + res.status(500).json({ error: 'Failed to update MCP config', code: 'UPDATE_MCP_CONFIG_FAILED' }); + } +}); + export default router; From 202c36f3a0929bdb698e916f34e9198c015c95ac Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Mon, 13 Jul 2026 16:32:53 +0800 Subject: [PATCH 02/11] feat(web): add MCP settings panel and bidirectional sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add McpConfig type + store actions/normalization, backendAdapter and autoSync push/pull for mcpConfig, a gated 'MCP服务' settings tab with enable toggle, endpoint display, token reset+copy and one-click client JSON copy. Electron renderer bridge pushes data snapshots and delegates semantic search to the renderer's vector index. --- src/App.tsx | 2 + src/components/SettingsPanel.tsx | 13 +- src/components/settings/McpSettings.tsx | 244 ++++++++++++++++++++++++ src/components/settings/index.ts | 1 + src/services/autoSync.ts | 22 ++- src/services/backendAdapter.ts | 30 ++- src/services/electronProxy.ts | 5 + src/services/mcpRendererBridge.ts | 166 ++++++++++++++++ src/store/useAppStore.ts | 43 +++++ src/types/index.ts | 10 + 10 files changed, 530 insertions(+), 6 deletions(-) create mode 100644 src/components/settings/McpSettings.tsx create mode 100644 src/services/mcpRendererBridge.ts diff --git a/src/App.tsx b/src/App.tsx index c2b0565c..6945f14b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,7 @@ import { logger } from './services/logger'; import { UpdateNotificationBanner } from './components/UpdateNotificationBanner'; import { backend } from './services/backendAdapter'; import { syncFromBackend, startAutoSync, stopAutoSync } from './services/autoSync'; +import { initMcpRendererBridge } from './services/mcpRendererBridge'; import type { AppState, SearchFilters } from './types'; /** @@ -144,6 +145,7 @@ function App() { const initBackend = async () => { try { await backend.init(); + initMcpRendererBridge(); if (backend.isAvailable && !cancelled) { await syncFromBackend(); if (!cancelled) { diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx index e73640b9..8ff87f8a 100644 --- a/src/components/SettingsPanel.tsx +++ b/src/components/SettingsPanel.tsx @@ -13,6 +13,7 @@ import { ScrollText, Layout, Search, + Plug, } from 'lucide-react'; import { useAppStore } from '../store/useAppStore'; import { isElectron } from '../services/electronProxy'; @@ -29,9 +30,10 @@ import { DiagnosticLogsPanel, MenuManagementPanel, VectorSearchSettings, + McpSettings, } from './settings'; -type SettingsTab = 'general' | 'ai' | 'webdav' | 'backup' | 'backend' | 'category' | 'menu' | 'data' | 'logs' | 'network' | 'vectorSearch'; +type SettingsTab = 'general' | 'ai' | 'webdav' | 'backup' | 'backend' | 'category' | 'menu' | 'data' | 'logs' | 'network' | 'vectorSearch' | 'mcp'; interface SettingsTabItem { id: SettingsTab; @@ -258,7 +260,7 @@ export const SettingsPanel: React.FC = ({ // Valid SettingsTab values for runtime validation const VALID_TABS: ReadonlySet = useMemo( - () => new Set(['general', 'ai', 'webdav', 'backup', 'backend', 'category', 'menu', 'data', 'logs', 'network', 'vectorSearch']), + () => new Set(['general', 'ai', 'webdav', 'backup', 'backend', 'category', 'menu', 'data', 'logs', 'network', 'vectorSearch', 'mcp']), [] ); @@ -361,6 +363,11 @@ export const SettingsPanel: React.FC = ({ label: t('向量搜索', 'Vector Search'), icon: , }, + ...((isElectron() || backend.isAvailable) ? [{ + id: 'mcp' as SettingsTab, + label: t('MCP服务', 'MCP Server'), + icon: , + }] : []), ]; const renderTabContent = () => { @@ -388,6 +395,8 @@ export const SettingsPanel: React.FC = ({ return ; case 'vectorSearch': return ; + case 'mcp': + return ; default: return null; } diff --git a/src/components/settings/McpSettings.tsx b/src/components/settings/McpSettings.tsx new file mode 100644 index 00000000..fc67391d --- /dev/null +++ b/src/components/settings/McpSettings.tsx @@ -0,0 +1,244 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { Plug, Copy, RefreshCw, Check, KeyRound } from 'lucide-react'; +import { useAppStore } from '../../store/useAppStore'; +import { isElectron } from '../../services/electronProxy'; +import { backend } from '../../services/backendAdapter'; + +interface McpSettingsProps { + t: (zh: string, en: string) => string; +} + +function generateToken(): string { + const arr = new Uint8Array(24); + crypto.getRandomValues(arr); + return Array.from(arr, (b) => b.toString(16).padStart(2, '0')).join(''); +} + +export const McpSettings: React.FC = ({ t }) => { + const mcpConfig = useAppStore((s) => s.mcpConfig); + const setMcpConfig = useAppStore((s) => s.setMcpConfig); + + const [copied, setCopied] = useState(false); + const [tokenCopied, setTokenCopied] = useState(false); + + const endpoint = useMemo(() => { + if (isElectron()) { + return `http://127.0.0.1:${mcpConfig.port || 18789}/mcp`; + } + const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000'; + return `${origin}/mcp`; + }, [mcpConfig.port]); + + const clientConfig = useMemo( + () => ({ + mcpServers: { + 'github-stars-manager': { + type: 'http', + url: endpoint, + headers: { Authorization: `Bearer ${mcpConfig.token || ''}` }, + }, + }, + }), + [endpoint, mcpConfig.token] + ); + + const isElectronMode = isElectron(); + + const applyConfig = useCallback( + (next: Partial) => { + const merged = { ...mcpConfig, ...next }; + setMcpConfig(merged); + if (backend.isAvailable) { + backend.syncMcpConfig(merged).catch(() => undefined); + } + if (isElectronMode) { + if (merged.enabled) { + window.electronAPI?.startMcp?.({ port: merged.port, token: merged.token }); + } else { + window.electronAPI?.stopMcp?.(); + } + } + }, + [mcpConfig, isElectronMode, setMcpConfig] + ); + + const handleToggle = useCallback(() => { + const enabled = !mcpConfig.enabled; + const token = enabled && !mcpConfig.token ? generateToken() : mcpConfig.token; + applyConfig({ enabled, token }); + }, [mcpConfig.enabled, mcpConfig.token, applyConfig]); + + const handleResetToken = useCallback(async () => { + const token = generateToken(); + applyConfig({ token }); + try { + await navigator.clipboard.writeText( + JSON.stringify( + { + mcpServers: { + 'github-stars-manager': { + type: 'http', + url: endpoint, + headers: { Authorization: `Bearer ${token}` }, + }, + }, + }, + null, + 2 + ) + ); + setTokenCopied(true); + setTimeout(() => setTokenCopied(false), 2000); + } catch { + /* clipboard may be unavailable */ + } + }, [applyConfig, endpoint]); + + const handleCopyConfig = useCallback(async () => { + try { + await navigator.clipboard.writeText(JSON.stringify(clientConfig, null, 2)); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + /* clipboard may be unavailable */ + } + }, [clientConfig]); + + return ( +
+
+

+ + {t('MCP 服务', 'MCP Server')} +

+

+ {t( + '允许 AI Agent(如 Claude Desktop、Cursor)通过 MCP 协议读取你收藏并加工过的仓库信息,无需手动使用命令行。', + 'Let AI agents (e.g. Claude Desktop, Cursor) read your curated starred repositories over MCP — no manual CLI needed.' + )} +

+
+ + {/* 启用开关 */} +
+
+
+ {t('启用 MCP 服务', 'Enable MCP Server')} +
+
+ {t('开启后,Agent 可通过下方端点连接本软件', 'Once enabled, agents can connect via the endpoint below')} +
+
+ +
+ + {mcpConfig.enabled && ( +
+ {/* 端点信息 */} +
+
+ +
+ + {endpoint} + +
+

+ {t( + '支持 Streamable HTTP;旧客户端可回退到 SSE:', + 'Streamable HTTP is primary; older clients may fall back to SSE at:' + )}{' '} + + {endpoint.replace(/\/mcp$/, '/mcp/sse')} + +

+
+ + {/* 桌面端端口 */} + {isElectronMode && ( +
+ + applyConfig({ port: Number(e.target.value) || 18789 })} + className="w-32 px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-sm text-gray-900 dark:text-text-primary focus:outline-none focus:ring-2 focus:ring-brand-indigo/50" + /> +

+ {t('仅绑定 127.0.0.1,仅本机可访问', 'Bound to 127.0.0.1 — localhost only')} +

+
+ )} +
+ + {/* 令牌 */} +
+
+ + {t('访问令牌', 'Access Token')} +
+

+ {t( + 'Agent 连接时需携带此令牌(Bearer)。如需更换,点击重置即可生成新令牌。', + 'Agents must present this token as Bearer. Click reset to generate a new one whenever you like.' + )} +

+
+ + {mcpConfig.token ? `${mcpConfig.token.slice(0, 8)}…${mcpConfig.token.slice(-4)}` : t('未生成', 'not generated')} + + +
+
+ + {/* 一键复制配置 */} +
+
+ {t('客户端配置(一键复制)', 'Client Config (copy in one click)')} +
+

+ {t( + '将以下 JSON 粘贴到支持 MCP 的客户端(如 Claude Desktop、Cursor)的配置文件中即可使用。', + 'Paste this JSON into your MCP-capable client (e.g. Claude Desktop, Cursor) config file.' + )} +

+
+              {JSON.stringify(clientConfig, null, 2)}
+            
+ +
+
+ )} +
+ ); +}; + +export default McpSettings; diff --git a/src/components/settings/index.ts b/src/components/settings/index.ts index 8a08ccc4..04cf2a15 100644 --- a/src/components/settings/index.ts +++ b/src/components/settings/index.ts @@ -9,3 +9,4 @@ export { NetworkPanel } from './NetworkPanel'; export { DiagnosticLogsPanel } from './DiagnosticLogsPanel'; export { MenuManagementPanel } from './MenuManagementPanel'; export { VectorSearchSettings } from './VectorSearchSettings'; +export { McpSettings } from './McpSettings'; diff --git a/src/services/autoSync.ts b/src/services/autoSync.ts index 01e51b25..88d29452 100644 --- a/src/services/autoSync.ts +++ b/src/services/autoSync.ts @@ -35,6 +35,7 @@ const _lastHash = { webdav: '', embedding: '', vectorSearch: '', + mcp: '', settings: '', }; @@ -67,17 +68,18 @@ export async function syncFromBackend(): Promise { const startTime = Date.now(); try { - const [reposResult, releasesResult, aiResult, webdavResult, embeddingResult, vectorSearchResult, settingsResult] = await Promise.allSettled([ + const [reposResult, releasesResult, aiResult, webdavResult, embeddingResult, vectorSearchResult, mcpResult, settingsResult] = await Promise.allSettled([ backend.fetchRepositories(), backend.fetchReleases(), backend.fetchAIConfigs(), backend.fetchWebDAVConfigs(), backend.fetchEmbeddingConfigs(), backend.fetchVectorSearchConfig(), + backend.fetchMcpConfig(), backend.fetchSettings(), ]); - const changed = { repos: false, releases: false, ai: false, webdav: false, embedding: false, vectorSearch: false, settings: false }; + const changed = { repos: false, releases: false, ai: false, webdav: false, embedding: false, vectorSearch: false, mcp: false, settings: false }; // Compute hashes for each slice — only mark changed if hash differs const hashes: Record = {}; @@ -129,6 +131,14 @@ export async function syncFromBackend(): Promise { } } + if (mcpResult.status === 'fulfilled') { + const hash = quickHash(mcpResult.value); + if (hash !== _lastHash.mcp) { + hashes.mcp = hash; + changed.mcp = true; + } + } + if (settingsResult.status === 'fulfilled') { const hash = quickHash(settingsResult.value); if (hash !== _lastHash.settings) { @@ -239,6 +249,10 @@ export async function syncFromBackend(): Promise { state.setVectorSearchConfig(backendConfig); _lastHash.vectorSearch = hashes.vectorSearch; } + if (changed.mcp && mcpResult.status === 'fulfilled') { + state.setMcpConfig(mcpResult.value); + _lastHash.mcp = hashes.mcp; + } // Sync active selections from settings if (changed.settings && settingsResult.status === 'fulfilled') { const settings = settingsResult.value; @@ -326,6 +340,7 @@ export async function syncToBackend(): Promise { backend.syncWebDAVConfigs(state.webdavConfigs), backend.syncEmbeddingConfigs(state.embeddingConfigs), backend.syncVectorSearchConfig(state.vectorSearchConfig), + backend.syncMcpConfig(state.mcpConfig), backend.syncSettings({ activeAIConfig: state.activeAIConfig, activeWebDAVConfig: state.activeWebDAVConfig, @@ -338,7 +353,7 @@ export async function syncToBackend(): Promise { collapsedSidebarCategoryCount: state.collapsedSidebarCategoryCount, }), ]); - const [reposSync, releasesSync, aiSync, webdavSync, embeddingSync, vectorSearchSync, settingsSync] = results; + const [reposSync, releasesSync, aiSync, webdavSync, embeddingSync, vectorSearchSync, mcpSync, settingsSync] = results; const failures = results.filter(r => r.status === 'rejected'); if (failures.length > 0) { @@ -356,6 +371,7 @@ export async function syncToBackend(): Promise { if (webdavSync.status === 'fulfilled') _lastHash.webdav = quickHash(state.webdavConfigs); if (embeddingSync.status === 'fulfilled') _lastHash.embedding = quickHash(state.embeddingConfigs); if (vectorSearchSync.status === 'fulfilled') _lastHash.vectorSearch = quickHash(state.vectorSearchConfig); + if (mcpSync.status === 'fulfilled') _lastHash.mcp = quickHash(state.mcpConfig); if (settingsSync.status === 'fulfilled') { _lastHash.settings = quickHash({ activeAIConfig: state.activeAIConfig, diff --git a/src/services/backendAdapter.ts b/src/services/backendAdapter.ts index a0f38c14..8e964322 100644 --- a/src/services/backendAdapter.ts +++ b/src/services/backendAdapter.ts @@ -1,7 +1,7 @@ import { translateBackendError } from '../utils/backendErrors'; import { logger } from './logger'; -import { Repository, Release, AIConfig, WebDAVConfig, EmbeddingConfig, VectorSearchConfig } from '../types'; +import { Repository, Release, AIConfig, WebDAVConfig, EmbeddingConfig, VectorSearchConfig, McpConfig } from '../types'; import { useAppStore } from '../store/useAppStore'; import { isReadmeCandidateItem, type GitHubReadmeCandidateItem } from '../utils/readmeVariants'; @@ -650,6 +650,34 @@ class BackendAdapter { } + // === MCP Config === + + async syncMcpConfig(config: McpConfig): Promise { + if (!this._backendUrl) return; + + const res = await this.fetchWithRetry(`${this._backendUrl}/configs/mcp`, { + method: 'PUT', + headers: this.getAuthHeaders(), + body: JSON.stringify(config) + }, 30000, 3); + if (!res.ok) await this.throwTranslatedError(res, 'Sync MCP config error'); + } + + async fetchMcpConfig(): Promise { + if (!this._backendUrl) throw new Error('Backend not available'); + + const res = await this.fetchWithTimeout(`${this._backendUrl}/configs/mcp?decrypt=true`, { + headers: this.getAuthHeaders() + }); + if (!res.ok) await this.throwTranslatedError(res, 'Fetch MCP config error'); + const data = await res.json() as Record; + return { + enabled: data.enabled === true || data.enabled === 1, + port: typeof data.port === 'number' ? data.port : 0, + token: typeof data.token === 'string' ? data.token : '', + }; + } + // === Settings (active selections) === async syncSettings(settings: Record): Promise { diff --git a/src/services/electronProxy.ts b/src/services/electronProxy.ts index 3421691d..ed4c4ee0 100644 --- a/src/services/electronProxy.ts +++ b/src/services/electronProxy.ts @@ -4,6 +4,11 @@ interface ElectronAPI { setProxy: (config: ProxyConfig) => Promise<{ success: boolean }>; getProxy: () => Promise; testProxy: (config: ProxyConfig) => Promise<{ success: boolean; error?: string }>; + // MCP 服务(桌面端,可选;仅在 Electron 主进程内置 MCP 时存在) + startMcp?: (cfg: { port: number; token: string }) => Promise<{ success: boolean }>; + stopMcp?: () => Promise; + pushMcpSnapshot?: (snapshot: unknown) => Promise; + mcpSemanticSearch?: (query: string, topK: number) => Promise>; } declare global { diff --git a/src/services/mcpRendererBridge.ts b/src/services/mcpRendererBridge.ts new file mode 100644 index 00000000..e50c3680 --- /dev/null +++ b/src/services/mcpRendererBridge.ts @@ -0,0 +1,166 @@ +import { useAppStore } from '../store/useAppStore'; +import { isElectron } from './electronProxy'; +import { EmbeddingClient, VectorSearchService } from './vectorSearchService'; + +interface McpSnapshotRepo { + fullName: string; + name: string; + description: string | null; + htmlUrl: string; + stars: number; + language: string | null; + topics: string[]; + customTags: string[]; + aiTags: string[]; + platforms: string[]; + category: string | null; + updatedAt: string | null; + analyzed: boolean; + customDescription: string | null; + aiSummary: string | null; + starredAt: string | null; +} + +interface McpSnapshot { + repositories: McpSnapshotRepo[]; + releases: Array<{ + repoFullName: string; + repoName: string; + tagName: string; + name: string | null; + publishedAt: string | null; + htmlUrl: string; + prerelease: boolean; + isRead: boolean; + }>; + categories: Array<{ id: string; name: string; count: number }>; + vectorEnabled: boolean; +} + +function parseArray(value: unknown): string[] { + if (Array.isArray(value)) return value.map((v) => String(v)); + if (typeof value === 'string') { + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed.map((v) => String(v)) : []; + } catch { + return []; + } + } + return []; +} + +function buildSnapshot(): McpSnapshot { + const state = useAppStore.getState(); + const repos = state.repositories.map((r) => ({ + fullName: r.full_name, + name: r.name, + description: r.description ?? null, + htmlUrl: r.html_url, + stars: r.stargazers_count ?? 0, + language: r.language ?? null, + topics: parseArray(r.topics), + customTags: parseArray(r.custom_tags), + aiTags: parseArray(r.ai_tags), + platforms: parseArray(r.ai_platforms), + category: r.custom_category ?? null, + updatedAt: r.updated_at ?? null, + analyzed: !!r.analyzed_at, + customDescription: r.custom_description ?? null, + aiSummary: r.ai_summary ?? null, + starredAt: r.starred_at ?? null, + })); + + const predefined = (state.customCategories ?? []).map((c) => ({ id: c.id, name: c.name, count: 0 })); + const countMap = new Map(); + for (const r of repos) { + if (r.category) countMap.set(r.category, (countMap.get(r.category) ?? 0) + 1); + } + const categories = predefined.map((p) => ({ ...p, count: countMap.get(p.name) ?? 0 })); + for (const [name, count] of countMap) { + if (!predefined.some((p) => p.name === name)) { + categories.push({ id: name, name, count }); + } + } + + return { + repositories: repos, + releases: state.releases.map((rel) => ({ + repoFullName: rel.repository?.full_name ?? '', + repoName: rel.repository?.name ?? '', + tagName: rel.tag_name ?? '', + name: rel.name ?? null, + publishedAt: rel.published_at ?? null, + htmlUrl: rel.html_url ?? '', + prerelease: !!rel.prerelease, + isRead: !!rel.is_read, + })), + categories, + vectorEnabled: !!( + state.vectorSearchConfig.enabled && + state.vectorSearchConfig.workerUrl && + state.vectorSearchConfig.embeddingConfigId + ), + }; +} + +/** + * Electron 专用:将收藏数据以快照形式推送给主进程内置的 MCP 服务, + * 并暴露语义搜索委托(由渲染进程复用已有向量索引能力)。 + * 纯前端 / 后端模式下此函数为空操作。 + */ +export function initMcpRendererBridge(): void { + if (!isElectron()) return; + + (window as unknown as Record).__gsmMcpSemanticSearch = async ( + query: string, + topK = 10 + ): Promise> => { + const state = useAppStore.getState(); + const vs = state.vectorSearchConfig; + if (!vs.enabled || !vs.workerUrl || !vs.embeddingConfigId) return []; + const emb = state.embeddingConfigs.find((c) => c.id === vs.embeddingConfigId); + if (!emb) return []; + try { + const client = new EmbeddingClient(emb); + const svc = new VectorSearchService(vs); + const vectors = await client.embed([query], 'query'); + if (!vectors || vectors.length === 0) return []; + const matches = await svc.query(vectors[0], { topK, threshold: 0.3 }); + return matches + .map((m) => ({ + fullName: String((m.metadata as { full_name?: string })?.full_name ?? m.id ?? ''), + score: typeof m.score === 'number' ? m.score : 0, + })) + .filter((m) => !!m.fullName); + } catch { + return []; + } + }; + + let debounceTimer: ReturnType | null = null; + const push = () => { + const state = useAppStore.getState(); + if (!state.mcpConfig.enabled) return; + window.electronAPI?.pushMcpSnapshot?.(buildSnapshot()); + }; + + // 启动时若已启用 MCP,则恢复主进程的 MCP 服务并推送初始快照 + const initial = useAppStore.getState(); + if (initial.mcpConfig.enabled && initial.mcpConfig.token) { + window.electronAPI?.startMcp?.({ port: initial.mcpConfig.port, token: initial.mcpConfig.token }); + window.electronAPI?.pushMcpSnapshot?.(buildSnapshot()); + } + + useAppStore.subscribe((state, prev) => { + if (state.mcpConfig.enabled && (!prev.mcpConfig.enabled || state.mcpConfig.token !== prev.mcpConfig.token)) { + // 刚启用或令牌变更,立即推送一次 + window.electronAPI?.startMcp?.({ port: state.mcpConfig.port, token: state.mcpConfig.token }); + window.electronAPI?.pushMcpSnapshot?.(buildSnapshot()); + return; + } + if (!state.mcpConfig.enabled) return; + if (debounceTimer) clearTimeout(debounceTimer); + debounceTimer = setTimeout(push, 2000); + }); +} diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts index 0b109800..bfa060cd 100644 --- a/src/store/useAppStore.ts +++ b/src/store/useAppStore.ts @@ -13,6 +13,7 @@ import { EmbeddingConfig, VectorSearchConfig, VectorSearchStatus, + McpConfig, VectorIndexingState, ProxyConfig, RpcDownloadConfig, @@ -331,6 +332,9 @@ interface AppActions { setVectorSearchStatus: (status: VectorSearchStatus | undefined) => void; setVectorIndexingState: (state: Partial) => void; + // MCP 服务 actions + setMcpConfig: (config: Partial) => void; + // Similar repositories view actions enterSimilarView: (repos: Repository[], anchor: Repository) => void; resetSimilarView: () => void; @@ -659,6 +663,34 @@ const mergeVectorSearchConfig = ( }; }; +// MCP 服务默认配置 +const DEFAULT_MCP_PORT = 18789; +const defaultMcpConfig: McpConfig = { + enabled: false, + port: DEFAULT_MCP_PORT, + token: '', +}; + +const normalizeMcpConfig = (raw: unknown): McpConfig => { + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) { + return { ...defaultMcpConfig }; + } + const config = raw as Record; + const port = typeof config.port === 'number' && Number.isInteger(config.port) && config.port > 0 + ? config.port + : DEFAULT_MCP_PORT; + return { + enabled: config.enabled === true, + port, + token: typeof config.token === 'string' ? config.token : '', + }; +}; + +const mergeMcpConfig = (current: McpConfig, patch: Partial): McpConfig => ({ + ...current, + ...patch, +}); + export const normalizePersistedState = ( persisted: PersistedAppState | undefined, currentState: AppState & AppActions @@ -1140,6 +1172,7 @@ export const useAppStore = create()( activeEmbeddingConfig: null, vectorSearchConfig: { ...defaultVectorSearchConfig }, vectorSearchStatus: { connected: false, vectorCount: 0, dimensions: 0 }, + mcpConfig: { ...defaultMcpConfig }, vectorIndexingState: { isIndexing: false, phase: null, phaseDone: 0, phaseTotal: 0, result: null }, similarView: null, webdavConfigs: [], @@ -1504,6 +1537,11 @@ export const useAppStore = create()( vectorIndexingState: { ...state.vectorIndexingState, ...indexingState } })), + // MCP 服务 actions + setMcpConfig: (config) => set((state) => ({ + mcpConfig: mergeMcpConfig(state.mcpConfig, config) + })), + // Similar repositories view actions /** * 进入"查找相似仓库"视图:保存当前相似结果、锚点仓库,并快照进入前的 @@ -2195,6 +2233,9 @@ export const useAppStore = create()( // 持久化向量搜索状态(vectorCount 等,跨重启保留) vectorSearchStatus: state.vectorSearchStatus, + // 持久化 MCP 服务配置 + mcpConfig: state.mcpConfig, + // 持久化WebDAV配置 webdavConfigs: state.webdavConfigs, activeWebDAVConfig: state.activeWebDAVConfig, @@ -2454,6 +2495,8 @@ export const useAppStore = create()( stateRecord.vectorSearchConfig, stateRecord.embeddingConfigs ); + // 初始化/迁移 mcpConfig + stateRecord.mcpConfig = normalizeMcpConfig(stateRecord.mcpConfig); } return state as PersistedAppState; diff --git a/src/types/index.ts b/src/types/index.ts index 06a92c7e..d78d3509 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -319,6 +319,13 @@ export interface RpcDownloadConfig { secret?: string; } +// MCP 服务配置(只读,Streamable HTTP / SSE 供 agent 调用) +export interface McpConfig { + enabled: boolean; + port: number; // 桌面端监听端口(后端模式忽略,挂在后端同端口 /mcp) + token: string; // 鉴权令牌,首次启用时生成,可重置 +} + export interface SearchFilters { query: string; tags: string[]; @@ -404,6 +411,9 @@ export interface AppState { vectorSearchStatus?: VectorSearchStatus; vectorIndexingState: VectorIndexingState; + // MCP 服务 + mcpConfig: McpConfig; + // Similar repositories view (triggered by "查找相似仓库") similarView: SimilarViewState | null; From f2d897084066ed6bec5b30a3b2d87d3fdf9c2f27 Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Mon, 13 Jul 2026 16:33:02 +0800 Subject: [PATCH 03/11] feat(desktop): embed self-contained MCP server in Electron build Rewrite build-desktop.js to install @modelcontextprotocol/sdk + zod into electron/, embed a self-contained mcpServer.mjs (Streamable HTTP + SSE fallback, snapshot-based provider, semantic search delegated to the renderer), and wire IPC (mcp:start/stop/snapshot/semanticSearch). SSE endpoints now require the bearer token; MCP auto-starts on launch when previously enabled. --- scripts/build-desktop.js | 325 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 310 insertions(+), 15 deletions(-) diff --git a/scripts/build-desktop.js b/scripts/build-desktop.js index 8ad5b70e..8605dc80 100644 --- a/scripts/build-desktop.js +++ b/scripts/build-desktop.js @@ -19,11 +19,12 @@ if (!fs.existsSync(electronDir)) { // 3. 创建主进程文件 const mainJs = ` -const { app, BrowserWindow, Menu, shell } = require('electron'); +const { app, BrowserWindow, Menu, shell, ipcMain } = require('electron'); const path = require('path'); const isDev = process.env.NODE_ENV === 'development'; let mainWindow; +let mcpModule = null; function createWindow() { mainWindow = new BrowserWindow({ @@ -121,17 +122,51 @@ function createWindow() { }); } -app.whenReady().then(createWindow); +app.whenReady().then(async () => { + createWindow(); -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { - app.quit(); + // 加载内置 MCP 服务模块(仅当用户在设置中启用 MCP 时才会真正监听端口) + try { + mcpModule = await import('./mcpServer.mjs'); + mcpModule.setSemanticHandler(async (query, topK) => { + const win = BrowserWindow.getAllWindows()[0]; + if (!win) return []; + try { + return await win.webContents.executeJavaScript( + 'window.__gsmMcpSemanticSearch ? await window.__gsmMcpSemanticSearch(' + + JSON.stringify(query) + ', ' + JSON.stringify(topK) + ') : []' + ); + } catch { + return []; + } + }); + + ipcMain.handle('mcp:start', async (_e, cfg) => { + try { + await mcpModule.startMcp(cfg); + return { success: true }; + } catch (e) { + return { success: false, error: String(e) }; + } + }); + ipcMain.handle('mcp:stop', async () => { + try { await mcpModule.stopMcp(); } catch { /* ignore */ } + }); + ipcMain.on('mcp:snapshot', (_e, snapshot) => { + mcpModule?.setSnapshot(snapshot); + }); + } catch (e) { + console.error('[GSM] Failed to load MCP module:', e); } + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + }); }); -app.on('activate', () => { - if (BrowserWindow.getAllWindows().length === 0) { - createWindow(); +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); } }); @@ -146,31 +181,291 @@ app.on('web-contents-created', (event, contents) => { fs.writeFileSync(path.join(electronDir, 'main.js'), mainJs); -// 4. 创建Electron package.json +// 4. 创建 MCP 服务模块(自包含,使用官方 SDK + zod) +const mcpServerMjs = ` +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import { z } from 'zod'; +import crypto from 'node:crypto'; +import http from 'node:http'; + +let snapshot = { repositories: [], releases: [], categories: [], vectorEnabled: false }; +let authToken = ''; +let httpServer = null; +let semanticHandler = null; + +export function setSnapshot(s) { + if (s && typeof s === 'object') snapshot = s; +} + +export function setSemanticHandler(fn) { + semanticHandler = fn; +} + +function json(content) { + return { content: [{ type: 'text', text: typeof content === 'string' ? content : JSON.stringify(content, null, 2) }] }; +} + +function extractBearer(req) { + const h = req.headers['authorization'] || ''; + return typeof h === 'string' && h.toLowerCase().startsWith('bearer ') ? h.slice(7).trim() : ''; +} + +// Constant-time comparison (matches the existing API auth pattern) +function tokenMatches(provided, expected) { + const a = Buffer.from(provided || ''); + const b = Buffer.from(expected || ''); + if (a.length !== b.length) return false; + return crypto.timingSafeEqual(a, b); +} + +function registerTools(server) { + server.registerTool( + 'search_repositories', + { + description: 'Search the user\\'s starred GitHub repositories stored by GitHub Stars Manager. Supports keyword query plus filters by tags, languages, categories, star range and AI-analyzed status.', + inputSchema: { + query: z.string().optional(), + tags: z.array(z.string()).optional(), + languages: z.array(z.string()).optional(), + categories: z.array(z.string()).optional(), + minStars: z.number().optional(), + maxStars: z.number().optional(), + isAnalyzed: z.boolean().optional(), + sortBy: z.enum(['stars', 'updated', 'name', 'starred']).optional(), + sortOrder: z.enum(['desc', 'asc']).optional(), + limit: z.number().optional() + } + }, + async (args) => json(searchRepos(args)) + ); + + server.registerTool( + 'get_repository', + { description: 'Get full detail of a starred repository by full_name (owner/repo).', inputSchema: { fullName: z.string() } }, + async (args) => { + const r = snapshot.repositories.find((x) => x.fullName === args.fullName); + return r ? json(r) : json({ error: 'Repository not found: ' + args.fullName }); + } + ); + + server.registerTool( + 'get_repository_comments', + { description: 'Get user-authored notes / custom description of a starred repository by full_name.', inputSchema: { fullName: z.string() } }, + async (args) => { + const r = snapshot.repositories.find((x) => x.fullName === args.fullName); + if (!r) return json({ error: 'Repository not found: ' + args.fullName }); + return json({ fullName: r.fullName, customDescription: r.customDescription, notes: r.customDescription }); + } + ); + + server.registerTool( + 'list_categories', + { description: 'List categories with repository counts.', inputSchema: {} }, + async () => json(snapshot.categories || []) + ); + + server.registerTool( + 'list_tags', + { description: 'List the most used tags across starred repositories with counts.', inputSchema: {} }, + async () => json(topTags()) + ); + + server.registerTool( + 'list_releases', + { description: 'List recent releases of starred repositories, optionally filtered by publish date (ISO).', inputSchema: { since: z.string().optional(), limit: z.number().optional() } }, + async (args) => { + let rels = snapshot.releases || []; + if (args.since) rels = rels.filter((r) => r.publishedAt && r.publishedAt >= args.since); + return json(rels.slice(0, Math.min(args.limit || 50, 200))); + } + ); + + server.registerTool( + 'get_stats', + { description: 'Aggregate statistics of starred repositories.', inputSchema: {} }, + async () => json(getStats()) + ); + + server.registerTool( + 'semantic_search_repositories', + { description: 'Semantic / natural-language search over starred repositories using the configured vector index.', inputSchema: { query: z.string(), topK: z.number().optional() } }, + async (args) => { + if (!snapshot.vectorEnabled || !semanticHandler) { + return json({ disabled: true, message: 'Vector search is not configured in GitHub Stars Manager.' }); + } + try { + const results = await semanticHandler(args.query, args.topK || 10); + return json(results); + } catch { + return json([]); + } + } + ); +} + +function searchRepos(args) { + let results = snapshot.repositories || []; + const q = args.query ? args.query.trim().toLowerCase() : ''; + if (q) { + results = results.filter((r) => + [r.fullName, r.description, r.customDescription, (r.aiSummary || '')].filter(Boolean).some((t) => String(t).toLowerCase().includes(q)) || + [...(r.topics || []), ...(r.customTags || []), ...(r.aiTags || [])].some((t) => String(t).toLowerCase().includes(q)) + ); + } + if (args.languages && args.languages.length) results = results.filter((r) => r.language && args.languages.includes(r.language)); + if (args.categories && args.categories.length) results = results.filter((r) => r.category && args.categories.includes(r.category)); + if (typeof args.minStars === 'number') results = results.filter((r) => r.stars >= args.minStars); + if (typeof args.maxStars === 'number') results = results.filter((r) => r.stars <= args.maxStars); + if (args.isAnalyzed !== undefined) results = results.filter((r) => r.analyzed === args.isAnalyzed); + if (args.tags && args.tags.length) { + const tags = args.tags.map((t) => t.toLowerCase()); + results = results.filter((r) => [...(r.customTags || []), ...(r.aiTags || []), ...(r.topics || [])].some((t) => tags.includes(String(t).toLowerCase()))); + } + const sortBy = args.sortBy || 'stars'; + const dir = args.sortOrder === 'asc' ? 1 : -1; + results = [...results].sort((a, b) => { + const av = sortBy === 'name' ? a.fullName : sortBy === 'updated' ? a.updatedAt || '' : sortBy === 'starred' ? a.starredAt || '' : a.stars; + const bv = sortBy === 'name' ? b.fullName : sortBy === 'updated' ? b.updatedAt || '' : sortBy === 'starred' ? b.starredAt || '' : b.stars; + return av < bv ? -1 * dir : av > bv ? 1 * dir : 0; + }); + return results.slice(0, Math.min(args.limit || 50, 200)); +} + +function topTags() { + const tally = new Map(); + for (const r of snapshot.repositories || []) { + for (const t of [...(r.customTags || []), ...(r.aiTags || []), ...(r.topics || [])]) { + if (!t) continue; + tally.set(t, (tally.get(t) || 0) + 1); + } + } + return [...tally.entries()].map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count).slice(0, 100); +} + +function getStats() { + const repos = snapshot.repositories || []; + const byLanguage = {}; + const byCategory = {}; + for (const r of repos) { + if (r.language) byLanguage[r.language] = (byLanguage[r.language] || 0) + 1; + if (r.category) byCategory[r.category] = (byCategory[r.category] || 0) + 1; + } + return { totalRepositories: repos.length, analyzedCount: repos.filter((r) => r.analyzed).length, byLanguage, byCategory, topTags: topTags().slice(0, 20) }; +} + +async function handleRequest(server, transport, req, res, body) { + if (!tokenMatches(extractBearer(req), authToken)) { + res.statusCode = 401; + res.setHeader('content-type', 'application/json'); + res.end(JSON.stringify({ error: 'unauthorized' })); + return; + } + await server.connect(transport); + await transport.handleRequest(req, res, body); +} + +export async function startMcp({ port, token }) { + authToken = token || ''; + if (!authToken) throw new Error('MCP token is required'); + if (httpServer) await stopMcp(); + + const server = new McpServer({ name: 'github-stars-manager', version: '0.7.0' }, { capabilities: { tools: {} } }); + registerTools(server); + + const sseTransports = new Map(); + const httpServerInstance = http.createServer(async (req, res) => { + const url = new URL(req.url, 'http://localhost'); + let body = undefined; + if (req.method === 'POST' || req.method === 'PUT') { + const chunks = []; + for await (const c of req) chunks.push(c); + try { body = JSON.parse(Buffer.concat(chunks).toString() || '{}'); } catch { body = undefined; } + } + + try { + if (url.pathname === '/mcp') { + const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); + res.on('close', () => transport.close().catch(() => undefined)); + await handleRequest(server, transport, req, res, body); + } else if (url.pathname === '/mcp/sse' && req.method === 'GET') { + if (!tokenMatches(extractBearer(req), authToken)) { + res.statusCode = 401; + res.setHeader('content-type', 'application/json'); + res.end(JSON.stringify({ error: 'unauthorized' })); + return; + } + const transport = new SSEServerTransport('/mcp/sse/messages', res); + sseTransports.set(transport.sessionId, transport); + res.on('close', () => sseTransports.delete(transport.sessionId)); + await server.connect(transport); + await transport.start(); + } else if (url.pathname === '/mcp/sse/messages' && req.method === 'POST') { + if (!tokenMatches(extractBearer(req), authToken)) { + res.statusCode = 401; + res.setHeader('content-type', 'application/json'); + res.end(JSON.stringify({ error: 'unauthorized' })); + return; + } + const sid = url.searchParams.get('sessionId'); + const transport = sid ? sseTransports.get(sid) : undefined; + if (!transport) { res.statusCode = 404; res.end(JSON.stringify({ error: 'unknown session' })); return; } + await transport.handlePostMessage(req, res, body); + } else { + res.statusCode = 404; + res.end(JSON.stringify({ error: 'not found' })); + } + } catch (e) { + if (!res.headersSent) { res.statusCode = 500; res.end(JSON.stringify({ error: 'internal error' })); } + } + }); + + await new Promise((resolve) => httpServerInstance.listen(port, '127.0.0.1', resolve)); + httpServer = httpServerInstance; + console.log('[GSM] MCP server listening on 127.0.0.1:' + port + '/mcp'); +} + +export async function stopMcp() { + if (httpServer) { + await new Promise((resolve) => httpServer.close(resolve)); + httpServer = null; + } +} +`; + +fs.writeFileSync(path.join(electronDir, 'mcpServer.mjs'), mcpServerMjs); + +// 5. 创建Electron package.json(含 MCP 运行所需依赖) const electronPackageJson = { name: 'github-stars-manager-desktop', version: '1.0.0', description: 'GitHub Stars Manager Desktop App', main: 'main.js', + type: 'commonjs', author: 'GitHub Stars Manager', - license: 'MIT' + license: 'MIT', + dependencies: { + '@modelcontextprotocol/sdk': '^1.29.0', + zod: '^3.23.0' + } }; fs.writeFileSync( - path.join(electronDir, 'package.json'), + path.join(electronDir, 'package.json'), JSON.stringify(electronPackageJson, null, 2) ); -// 5. 安装Electron依赖 +// 6. 安装依赖(含 MCP SDK 与 zod,构建期完成,用户无感) console.log('📥 安装Electron依赖...'); try { - execSync('npm install --save-dev electron electron-builder', { stdio: 'inherit' }); + execSync('npm install', { stdio: 'inherit', cwd: electronDir }); } catch (error) { console.error('安装依赖失败:', error.message); process.exit(1); } -// 6. 构建应用 +// 7. 构建应用 console.log('🔨 构建桌面应用...'); try { execSync('npx electron-builder', { stdio: 'inherit' }); @@ -179,4 +474,4 @@ try { } catch (error) { console.error('构建失败:', error.message); process.exit(1); -} \ No newline at end of file +} From 66f7f4046a8b9f6698e46028e39ce78cb3d51226 Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Mon, 13 Jul 2026 16:33:02 +0800 Subject: [PATCH 04/11] chore(docker): expose backend port for local MCP clients Publish the backend container port to the host (default 3000) so local MCP clients (e.g. Claude Desktop, Cursor) can reach http://localhost:3000/mcp, while keeping the internal expose for the frontend. --- docker-compose.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 98260e8b..17b5f94d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,10 @@ services: image: ghcr.io/amintacccp/github-stars-manager-server:${BACKEND_IMAGE_TAG:-latest} # Uncomment below (and comment out 'image') to build locally instead: # build: ./server + ports: + # Published to the host so local MCP clients (e.g. Claude Desktop/Cursor) + # can reach the MCP endpoint at http://localhost:3000/mcp + - "${BACKEND_HOST_PORT:-3000}:3000" expose: - "3000" environment: From 82320938298e8dbd324f2a82a625612b289b13a8 Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Mon, 13 Jul 2026 17:01:26 +0800 Subject: [PATCH 05/11] fix(mcp): address gemini & coderabbit review audit - sqliteProvider: filter tags before pagination, fix Cohere embeddings shape, guard decrypt(apiKey) failure - mcp/index: read config per request, fresh McpServer per connection - configs: UPSERT preserves created_at; validate port 1-65535 - mcpRendererBridge: skip snapshot rebuild when data unchanged - autoSync: preserve local MCP token on decrypt_failed/empty - McpSettings: default port fallback + clamp input 1-65535 - build-desktop: close SSE transports on stop, auth before body buffer with 1MiB cap (413), handle listen EADDRINUSE, restore electron/electron-builder install step Refs #247 --- scripts/build-desktop.js | 118 +++++++++++++++------ server/src/mcp/index.ts | 132 +++++++++++++++--------- server/src/mcp/sqliteProvider.ts | 33 ++++-- server/src/routes/configs.ts | 20 +++- src/components/settings/McpSettings.tsx | 13 ++- src/services/autoSync.ts | 12 ++- src/services/mcpRendererBridge.ts | 9 ++ 7 files changed, 239 insertions(+), 98 deletions(-) diff --git a/scripts/build-desktop.js b/scripts/build-desktop.js index 8605dc80..197cdc7a 100644 --- a/scripts/build-desktop.js +++ b/scripts/build-desktop.js @@ -194,6 +194,8 @@ let snapshot = { repositories: [], releases: [], categories: [], vectorEnabled: let authToken = ''; let httpServer = null; let semanticHandler = null; +let sseTransports = new Map(); +const MAX_BODY_BYTES = 1024 * 1024; // 1 MiB cap on request bodies export function setSnapshot(s) { if (s && typeof s === 'object') snapshot = s; @@ -355,18 +357,7 @@ function getStats() { return { totalRepositories: repos.length, analyzedCount: repos.filter((r) => r.analyzed).length, byLanguage, byCategory, topTags: topTags().slice(0, 20) }; } -async function handleRequest(server, transport, req, res, body) { - if (!tokenMatches(extractBearer(req), authToken)) { - res.statusCode = 401; - res.setHeader('content-type', 'application/json'); - res.end(JSON.stringify({ error: 'unauthorized' })); - return; - } - await server.connect(transport); - await transport.handleRequest(req, res, body); -} - -export async function startMcp({ port, token }) { +async function startMcp({ port, token }) { authToken = token || ''; if (!authToken) throw new Error('MCP token is required'); if (httpServer) await stopMcp(); @@ -374,59 +365,109 @@ export async function startMcp({ port, token }) { const server = new McpServer({ name: 'github-stars-manager', version: '0.7.0' }, { capabilities: { tools: {} } }); registerTools(server); - const sseTransports = new Map(); + const listenPort = port && Number.isInteger(port) && port > 0 ? port : 18789; + const httpServerInstance = http.createServer(async (req, res) => { const url = new URL(req.url, 'http://localhost'); + const pathname = url.pathname; + + // Authenticate BEFORE buffering any body, so an unauthenticated caller + // cannot exhaust memory by streaming a huge payload. + const protectedPath = + pathname === '/mcp' || pathname === '/mcp/sse' || pathname === '/mcp/sse/messages'; + if (protectedPath && !tokenMatches(extractBearer(req), authToken)) { + res.statusCode = 401; + res.setHeader('content-type', 'application/json'); + res.end(JSON.stringify({ error: 'unauthorized' })); + return; + } + + // Buffer + parse body (capped) only for write methods that need it. let body = undefined; if (req.method === 'POST' || req.method === 'PUT') { const chunks = []; - for await (const c of req) chunks.push(c); - try { body = JSON.parse(Buffer.concat(chunks).toString() || '{}'); } catch { body = undefined; } + let size = 0; + try { + for await (const c of req) { + size += c.length; + if (size > MAX_BODY_BYTES) { + res.statusCode = 413; + res.setHeader('content-type', 'application/json'); + res.end(JSON.stringify({ error: 'payload too large' })); + req.destroy(); + return; + } + chunks.push(c); + } + const raw = Buffer.concat(chunks).toString('utf8'); + if (raw) { + try { body = JSON.parse(raw); } catch { body = undefined; } + } + } catch { + if (!res.headersSent) { + res.statusCode = 400; + res.setHeader('content-type', 'application/json'); + res.end(JSON.stringify({ error: 'bad request' })); + } + return; + } } try { - if (url.pathname === '/mcp') { + if (pathname === '/mcp') { const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); res.on('close', () => transport.close().catch(() => undefined)); - await handleRequest(server, transport, req, res, body); - } else if (url.pathname === '/mcp/sse' && req.method === 'GET') { - if (!tokenMatches(extractBearer(req), authToken)) { - res.statusCode = 401; - res.setHeader('content-type', 'application/json'); - res.end(JSON.stringify({ error: 'unauthorized' })); - return; - } + await server.connect(transport); + await transport.handleRequest(req, res, body); + } else if (pathname === '/mcp/sse' && req.method === 'GET') { const transport = new SSEServerTransport('/mcp/sse/messages', res); sseTransports.set(transport.sessionId, transport); res.on('close', () => sseTransports.delete(transport.sessionId)); await server.connect(transport); await transport.start(); - } else if (url.pathname === '/mcp/sse/messages' && req.method === 'POST') { - if (!tokenMatches(extractBearer(req), authToken)) { - res.statusCode = 401; + } else if (pathname === '/mcp/sse/messages' && req.method === 'POST') { + const sid = url.searchParams.get('sessionId'); + const transport = sid ? sseTransports.get(sid) : undefined; + if (!transport) { + res.statusCode = 404; res.setHeader('content-type', 'application/json'); - res.end(JSON.stringify({ error: 'unauthorized' })); + res.end(JSON.stringify({ error: 'unknown session' })); return; } - const sid = url.searchParams.get('sessionId'); - const transport = sid ? sseTransports.get(sid) : undefined; - if (!transport) { res.statusCode = 404; res.end(JSON.stringify({ error: 'unknown session' })); return; } await transport.handlePostMessage(req, res, body); } else { res.statusCode = 404; + res.setHeader('content-type', 'application/json'); res.end(JSON.stringify({ error: 'not found' })); } } catch (e) { - if (!res.headersSent) { res.statusCode = 500; res.end(JSON.stringify({ error: 'internal error' })); } + if (!res.headersSent) { + res.statusCode = 500; + res.setHeader('content-type', 'application/json'); + res.end(JSON.stringify({ error: 'internal error' })); + } } }); - await new Promise((resolve) => httpServerInstance.listen(port, '127.0.0.1', resolve)); + // Surface listen errors (e.g. EADDRINUSE) instead of crashing silently. + await new Promise((resolve, reject) => { + const onError = (err) => reject(err); + httpServerInstance.once('error', onError); + httpServerInstance.listen(listenPort, '127.0.0.1', () => { + httpServerInstance.removeListener('error', onError); + resolve(); + }); + }); httpServer = httpServerInstance; - console.log('[GSM] MCP server listening on 127.0.0.1:' + port + '/mcp'); + console.log('[GSM] MCP server listening on 127.0.0.1:' + listenPort + '/mcp'); } export async function stopMcp() { + // Close any open SSE transports first so clients are notified. + for (const transport of sseTransports.values()) { + try { await transport.close(); } catch { /* ignore */ } + } + sseTransports.clear(); if (httpServer) { await new Promise((resolve) => httpServer.close(resolve)); httpServer = null; @@ -465,6 +506,15 @@ try { process.exit(1); } +// 6b. 安装 electron + electron-builder(构建期完成,用户无感;原 main 分支的实现) +console.log('📥 安装 electron + electron-builder(构建依赖)...'); +try { + execSync('npm install --save-dev electron electron-builder', { stdio: 'inherit' }); +} catch (error) { + console.error('安装 electron/electron-builder 失败:', error.message); + process.exit(1); +} + // 7. 构建应用 console.log('🔨 构建桌面应用...'); try { diff --git a/server/src/mcp/index.ts b/server/src/mcp/index.ts index 8a12fd47..041b4eba 100644 --- a/server/src/mcp/index.ts +++ b/server/src/mcp/index.ts @@ -25,63 +25,84 @@ function tokenMatches(provided: string, expected: string): boolean { return crypto.timingSafeEqual(a, b); } +interface McpState { + db: ReturnType; + token: string; + vectorEnabled: boolean; +} + /** - * Mount the MCP server (Streamable HTTP at /mcp, SSE fallback at /mcp/sse) onto the - * Express app. No-op unless MCP is enabled AND a token is configured — existing routes - * and /api auth are untouched. + * Resolve the live MCP configuration from the database on every call. This keeps a + * single Express mount stable across config updates (no restart, no stale module-level + * server or cached token) and lets config edits take effect on the next request. + * Returns null when MCP is disabled or has no usable token. */ -export function initMcp(app: Express): void { - try { - const db = getDb(); - const row = db.prepare('SELECT * FROM mcp_configs WHERE id = ?').get('default') as - | Record - | undefined; - if (!row || !row.enabled) { - logger.info('mcp.init', 'MCP server disabled'); - return; - } +function loadMcpState(): McpState | null { + const db = getDb(); + const row = db.prepare('SELECT * FROM mcp_configs WHERE id = ?').get('default') as + | Record + | undefined; + if (!row || !row.enabled) return null; - let token = ''; - if (row.token_encrypted) { - try { - token = decrypt(row.token_encrypted as string, config.encryptionKey); - } catch { - token = ''; - } - } - if (!token) { - logger.warn('mcp.init', 'MCP enabled but no token configured; refusing to expose endpoint'); - return; + let token = ''; + if (row.token_encrypted) { + try { + token = decrypt(row.token_encrypted as string, config.encryptionKey); + } catch { + token = ''; } + } + if (!token) return null; - const vsRow = db - .prepare('SELECT * FROM vector_search_configs WHERE id = ?') - .get('default') as Record | undefined; - const vectorEnabled = !!( - vsRow && - vsRow.enabled && - vsRow.worker_url && - vsRow.embedding_config_id - ); + const vsRow = db + .prepare('SELECT * FROM vector_search_configs WHERE id = ?') + .get('default') as Record | undefined; + const vectorEnabled = !!( + vsRow && + vsRow.enabled && + vsRow.worker_url && + vsRow.embedding_config_id + ); + return { db, token, vectorEnabled }; +} - const provider = new SqliteMcpProvider(db); - const server = createMcpServer(provider, vectorEnabled); - const checkToken = (req: Request, res: Response): boolean => { - if (!tokenMatches(extractBearer(req), token)) { - res.status(401).json({ error: 'unauthorized' }); - return false; - } - return true; - }; +function unauthorized(res: Response): void { + res.status(401).json({ error: 'unauthorized' }); +} + +/** Build a fresh, isolated McpServer per request/connection. */ +function buildServer(state: McpState): ReturnType { + const provider = new SqliteMcpProvider(state.db); + return createMcpServer(provider, state.vectorEnabled); +} + +/** + * Mount the MCP server (Streamable HTTP at /mcp, SSE fallback at /mcp/sse) onto the + * Express app. Mounts unconditionally, but each request is gated on the live config — + * existing routes and /api auth are untouched. + */ +export function initMcp(app: Express): void { + try { + // Transports for in-flight SSE connections, keyed by session id. + const sseTransports = new Map(); // ── Streamable HTTP (primary, MCP 2025 spec) ── app.all('/mcp', async (req, res) => { - if (!checkToken(req, res)) return; + const state = loadMcpState(); + if (!state) { + res.status(404).json({ error: 'not found' }); + return; + } + if (!tokenMatches(extractBearer(req), state.token)) { + unauthorized(res); + return; + } try { const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); res.on('close', () => { transport.close().catch(() => undefined); }); + const server = buildServer(state); await server.connect(transport); await transport.handleRequest(req, res, req.body); } catch (err) { @@ -91,16 +112,23 @@ export function initMcp(app: Express): void { }); // ── SSE fallback (for clients not yet supporting Streamable HTTP) ── - const sseTransports = new Map(); - app.get('/mcp/sse', async (req, res) => { - if (!checkToken(req, res)) return; + const state = loadMcpState(); + if (!state) { + res.status(404).json({ error: 'not found' }); + return; + } + if (!tokenMatches(extractBearer(req), state.token)) { + unauthorized(res); + return; + } try { const transport = new SSEServerTransport('/mcp/sse/messages', res); sseTransports.set(transport.sessionId, transport); res.on('close', () => { sseTransports.delete(transport.sessionId); }); + const server = buildServer(state); await server.connect(transport); await transport.start(); } catch (err) { @@ -110,7 +138,15 @@ export function initMcp(app: Express): void { }); app.post('/mcp/sse/messages', async (req, res) => { - if (!checkToken(req, res)) return; + const state = loadMcpState(); + if (!state) { + res.status(404).json({ error: 'not found' }); + return; + } + if (!tokenMatches(extractBearer(req), state.token)) { + unauthorized(res); + return; + } const sessionId = req.query.sessionId as string | undefined; const transport = sessionId ? sseTransports.get(sessionId) : undefined; if (!transport) { @@ -125,7 +161,7 @@ export function initMcp(app: Express): void { } }); - logger.info('mcp.init', `MCP server mounted (vectorSearch=${vectorEnabled})`); + logger.info('mcp.init', 'MCP server mount registered (config read per request)'); } catch (err) { logger.errorFromError('mcp.init', 'Failed to init MCP server', err as Error); } diff --git a/server/src/mcp/sqliteProvider.ts b/server/src/mcp/sqliteProvider.ts index 09212b8e..62f61186 100644 --- a/server/src/mcp/sqliteProvider.ts +++ b/server/src/mcp/sqliteProvider.ts @@ -100,6 +100,11 @@ export class SqliteMcpProvider implements McpDataProvider { where.push(filter.isAnalyzed ? 'analyzed_at IS NOT NULL' : 'analyzed_at IS NULL'); } + // When a tag filter is present we cannot paginate in SQL first (matches could + // fall outside the page window), so we load candidates, filter in memory, then + // apply the caller's offset/limit. Otherwise we keep the SQL LIMIT/OFFSET. + const hasTagFilter = !!(filter.tags && filter.tags.length); + const sql = ` SELECT * FROM repositories ${where.length ? `WHERE ${where.join(' AND ')}` : ''} @@ -112,20 +117,22 @@ export class SqliteMcpProvider implements McpDataProvider { ? 'updated_at' : 'stargazers_count' } ${filter.sortOrder === 'asc' ? 'ASC' : 'DESC'} - LIMIT ? OFFSET ? + ${hasTagFilter ? '' : 'LIMIT ? OFFSET ?'} `; const limit = Math.min(Math.max(filter.limit ?? 50, 1), 200); const offset = Math.max(filter.offset ?? 0, 0); - params.push(limit, offset); + if (!hasTagFilter) params.push(limit, offset); const rows = this.db.prepare(sql).all(...params) as Record[]; let results = rows.map(mapSummary); - if (filter.tags && filter.tags.length) { - const tags = filter.tags.map((t) => t.toLowerCase()); - results = results.filter((r) => - [...r.customTags, ...r.aiTags, ...r.topics].some((t) => tags.includes(t.toLowerCase())) - ); + if (hasTagFilter) { + const tags = filter.tags!.map((t) => t.toLowerCase()); + results = results + .filter((r) => + [...r.customTags, ...r.aiTags, ...r.topics].some((t) => tags.includes(t.toLowerCase())) + ) + .slice(offset, offset + limit); } return Promise.resolve(results); } @@ -169,7 +176,15 @@ export class SqliteMcpProvider implements McpDataProvider { .get(v.embeddingConfigId) as Record | undefined; if (!embRow) return []; - const apiKey = embRow.api_key_encrypted ? decrypt(embRow.api_key_encrypted as string, config.encryptionKey) : ''; + let apiKey = ''; + if (embRow.api_key_encrypted) { + try { + apiKey = decrypt(embRow.api_key_encrypted as string, config.encryptionKey); + } catch { + // Corrupted key / encryption-key mismatch — semantic search cannot proceed + return []; + } + } const baseUrl = (embRow.base_url as string) ?? ''; const model = (embRow.model as string) ?? ''; const apiType = (embRow.api_type as string) ?? 'openai'; @@ -229,7 +244,7 @@ export class SqliteMcpProvider implements McpDataProvider { apiType === 'cohere' ? { model, texts: [text], input_type: 'search_query' } : { input: [text], model }, { headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, timeout: 15000 } ); - if (apiType === 'cohere') return (res.data?.embeddings?.[0]?.values as number[]) ?? null; + if (apiType === 'cohere') return (res.data?.embeddings?.[0] as number[]) ?? null; return (res.data?.data?.[0]?.embedding as number[]) ?? null; } catch { return null; diff --git a/server/src/routes/configs.ts b/server/src/routes/configs.ts index 0dfa7496..7a151b1d 100644 --- a/server/src/routes/configs.ts +++ b/server/src/routes/configs.ts @@ -808,12 +808,24 @@ router.put('/api/configs/mcp', (req, res) => { } const isEnabled = enabled === true || enabled === 1; - const mcpPort = typeof port === 'number' && port > 0 ? port : 0; + + if (typeof port !== 'number' || !Number.isInteger(port) || port < 1 || port > 65535) { + res.status(400).json({ + error: 'Invalid port: must be an integer between 1 and 65535', + code: 'INVALID_PORT', + }); + return; + } db.prepare(` - INSERT OR REPLACE INTO mcp_configs (id, enabled, port, token_encrypted, updated_at) - VALUES (?, ?, ?, ?, datetime('now')) - `).run('default', isEnabled ? 1 : 0, mcpPort, encryptedToken); + INSERT INTO mcp_configs (id, enabled, port, token_encrypted, created_at, updated_at) + VALUES (?, ?, ?, ?, datetime('now'), datetime('now')) + ON CONFLICT(id) DO UPDATE SET + enabled = excluded.enabled, + port = excluded.port, + token_encrypted = excluded.token_encrypted, + updated_at = datetime('now') + `).run('default', isEnabled ? 1 : 0, port, encryptedToken); res.json({ updated: true }); } catch (err) { diff --git a/src/components/settings/McpSettings.tsx b/src/components/settings/McpSettings.tsx index fc67391d..164ac6d9 100644 --- a/src/components/settings/McpSettings.tsx +++ b/src/components/settings/McpSettings.tsx @@ -53,7 +53,8 @@ export const McpSettings: React.FC = ({ t }) => { } if (isElectronMode) { if (merged.enabled) { - window.electronAPI?.startMcp?.({ port: merged.port, token: merged.token }); + // Port 0 / unset falls back to the default listen port in the main process. + window.electronAPI?.startMcp?.({ port: merged.port || 18789, token: merged.token }); } else { window.electronAPI?.stopMcp?.(); } @@ -176,8 +177,16 @@ export const McpSettings: React.FC = ({ t }) => { applyConfig({ port: Number(e.target.value) || 18789 })} + onChange={(e) => { + const raw = Number(e.target.value); + const clamped = Number.isNaN(raw) + ? 18789 + : Math.min(65535, Math.max(1, Math.trunc(raw))); + applyConfig({ port: clamped }); + }} className="w-32 px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-sm text-gray-900 dark:text-text-primary focus:outline-none focus:ring-2 focus:ring-brand-indigo/50" />

diff --git a/src/services/autoSync.ts b/src/services/autoSync.ts index 88d29452..d4450e5e 100644 --- a/src/services/autoSync.ts +++ b/src/services/autoSync.ts @@ -250,7 +250,17 @@ export async function syncFromBackend(): Promise { _lastHash.vectorSearch = hashes.vectorSearch; } if (changed.mcp && mcpResult.status === 'fulfilled') { - state.setMcpConfig(mcpResult.value); + const backendMcp = mcpResult.value; + const localMcp = state.mcpConfig; + // Preserve local token when the backend returns a masked/empty value or a + // decrypt failure, so a sync never overwrites a valid local token. + if ((backendMcp as Record).tokenStatus === 'decrypt_failed' || !backendMcp.token) { + if (localMcp.token) { + logger.warn('sync.decryptFailed', 'Backend decrypt_failed/empty for MCP token, preserving local value'); + (backendMcp as Record).token = localMcp.token; + } + } + state.setMcpConfig(backendMcp); _lastHash.mcp = hashes.mcp; } // Sync active selections from settings diff --git a/src/services/mcpRendererBridge.ts b/src/services/mcpRendererBridge.ts index e50c3680..5356ae1e 100644 --- a/src/services/mcpRendererBridge.ts +++ b/src/services/mcpRendererBridge.ts @@ -160,6 +160,15 @@ export function initMcpRendererBridge(): void { return; } if (!state.mcpConfig.enabled) return; + // Only schedule a (debounced) snapshot push when data the snapshot actually + // depends on changed. Plain UI state changes (e.g. selection, loading flags) + // reuse the same references, so we skip the expensive rebuild. + const relevantChanged = + state.repositories !== prev.repositories || + state.releases !== prev.releases || + state.customCategories !== prev.customCategories || + state.vectorSearchConfig !== prev.vectorSearchConfig; + if (!relevantChanged) return; if (debounceTimer) clearTimeout(debounceTimer); debounceTimer = setTimeout(push, 2000); }); From bbcd3cb7341dfea429586959dc1699a57e68a373 Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Mon, 13 Jul 2026 17:20:02 +0800 Subject: [PATCH 06/11] fix(mcp): address follow-up review comments (PR #247) - index.ts / mcpServer.mjs: drop redundant SSE transport.start() after server.connect() (avoids ERR_HTTP_HEADERS_SENT / 500s on /mcp/sse) - mcpRendererBridge: use 18789 port fallback for startMcp calls; include mcpConfig in snapshot rebuild reference-equality guard - build-desktop: register MCP IPC handlers before createWindow() so persisted enabled-state can't race handler registration; pin electron + electron-builder as devDependencies (drop implicit on-demand install) - docker-compose: bind backend host port to 127.0.0.1 (loopback) Refs #247 --- docker-compose.yml | 7 ++++--- package.json | 2 ++ scripts/build-desktop.js | 18 +++++------------- server/src/mcp/index.ts | 1 - src/services/mcpRendererBridge.ts | 7 ++++--- 5 files changed, 15 insertions(+), 20 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 17b5f94d..45b3bb05 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,9 +18,10 @@ services: # Uncomment below (and comment out 'image') to build locally instead: # build: ./server ports: - # Published to the host so local MCP clients (e.g. Claude Desktop/Cursor) - # can reach the MCP endpoint at http://localhost:3000/mcp - - "${BACKEND_HOST_PORT:-3000}:3000" + # Published to the localhost interface only (loopback) so local MCP + # clients (e.g. Claude Desktop/Cursor) can reach the MCP endpoint at + # http://localhost:3000/mcp without exposing it on all host interfaces. + - "127.0.0.1:${BACKEND_HOST_PORT:-3000}:3000" expose: - "3000" environment: diff --git a/package.json b/package.json index e562d565..efa7b66f 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,8 @@ "@vitejs/plugin-legacy": "^8.0.1", "@vitejs/plugin-react": "^6.0.0", "autoprefixer": "^10.4.18", + "electron": "^31.0.0", + "electron-builder": "^24.13.3", "concurrently": "^8.2.0", "eslint": "^9.9.1", "eslint-plugin-react-hooks": "^5.1.0-rc.0", diff --git a/scripts/build-desktop.js b/scripts/build-desktop.js index 197cdc7a..7ddc40bb 100644 --- a/scripts/build-desktop.js +++ b/scripts/build-desktop.js @@ -123,9 +123,9 @@ function createWindow() { } app.whenReady().then(async () => { - createWindow(); - - // 加载内置 MCP 服务模块(仅当用户在设置中启用 MCP 时才会真正监听端口) + // 加载内置 MCP 服务模块并注册 IPC 处理器(必须早于 createWindow, + // 否则渲染进程在启用 MCP 时发出的 mcp:start / mcp:snapshot 会早于 + // 主进程注册处理器,导致持久化的启用状态被错过。) try { mcpModule = await import('./mcpServer.mjs'); mcpModule.setSemanticHandler(async (query, topK) => { @@ -159,6 +159,8 @@ app.whenReady().then(async () => { console.error('[GSM] Failed to load MCP module:', e); } + createWindow(); + app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); @@ -424,7 +426,6 @@ async function startMcp({ port, token }) { sseTransports.set(transport.sessionId, transport); res.on('close', () => sseTransports.delete(transport.sessionId)); await server.connect(transport); - await transport.start(); } else if (pathname === '/mcp/sse/messages' && req.method === 'POST') { const sid = url.searchParams.get('sessionId'); const transport = sid ? sseTransports.get(sid) : undefined; @@ -506,15 +507,6 @@ try { process.exit(1); } -// 6b. 安装 electron + electron-builder(构建期完成,用户无感;原 main 分支的实现) -console.log('📥 安装 electron + electron-builder(构建依赖)...'); -try { - execSync('npm install --save-dev electron electron-builder', { stdio: 'inherit' }); -} catch (error) { - console.error('安装 electron/electron-builder 失败:', error.message); - process.exit(1); -} - // 7. 构建应用 console.log('🔨 构建桌面应用...'); try { diff --git a/server/src/mcp/index.ts b/server/src/mcp/index.ts index 041b4eba..9fadf888 100644 --- a/server/src/mcp/index.ts +++ b/server/src/mcp/index.ts @@ -130,7 +130,6 @@ export function initMcp(app: Express): void { }); const server = buildServer(state); await server.connect(transport); - await transport.start(); } catch (err) { logger.errorFromError('mcp.sse', 'SSE connection error', err as Error); if (!res.headersSent) res.status(500).json({ error: 'internal error' }); diff --git a/src/services/mcpRendererBridge.ts b/src/services/mcpRendererBridge.ts index 5356ae1e..fa3b750b 100644 --- a/src/services/mcpRendererBridge.ts +++ b/src/services/mcpRendererBridge.ts @@ -148,14 +148,14 @@ export function initMcpRendererBridge(): void { // 启动时若已启用 MCP,则恢复主进程的 MCP 服务并推送初始快照 const initial = useAppStore.getState(); if (initial.mcpConfig.enabled && initial.mcpConfig.token) { - window.electronAPI?.startMcp?.({ port: initial.mcpConfig.port, token: initial.mcpConfig.token }); + window.electronAPI?.startMcp?.({ port: initial.mcpConfig.port || 18789, token: initial.mcpConfig.token }); window.electronAPI?.pushMcpSnapshot?.(buildSnapshot()); } useAppStore.subscribe((state, prev) => { if (state.mcpConfig.enabled && (!prev.mcpConfig.enabled || state.mcpConfig.token !== prev.mcpConfig.token)) { // 刚启用或令牌变更,立即推送一次 - window.electronAPI?.startMcp?.({ port: state.mcpConfig.port, token: state.mcpConfig.token }); + window.electronAPI?.startMcp?.({ port: state.mcpConfig.port || 18789, token: state.mcpConfig.token }); window.electronAPI?.pushMcpSnapshot?.(buildSnapshot()); return; } @@ -167,7 +167,8 @@ export function initMcpRendererBridge(): void { state.repositories !== prev.repositories || state.releases !== prev.releases || state.customCategories !== prev.customCategories || - state.vectorSearchConfig !== prev.vectorSearchConfig; + state.vectorSearchConfig !== prev.vectorSearchConfig || + state.mcpConfig !== prev.mcpConfig; if (!relevantChanged) return; if (debounceTimer) clearTimeout(debounceTimer); debounceTimer = setTimeout(push, 2000); From 0cea6cd0aaa35a6f1d5e363beb11813cc2099550 Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Mon, 13 Jul 2026 17:24:23 +0800 Subject: [PATCH 07/11] chore: sync package-lock.json with electron/electron-builder devDeps Regenerate lock so CI `npm ci` passes after pinning electron + electron-builder as devDependencies in package.json. Refs #247 --- package-lock.json | 3377 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 3252 insertions(+), 125 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4620d13c..78ed80d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "github-stars-manager", - "version": "0.5.6", + "version": "0.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "github-stars-manager", - "version": "0.5.6", + "version": "0.7.0", "dependencies": { "@types/query-string": "^6.2.0", "date-fns": "^3.3.1", @@ -34,6 +34,8 @@ "@vitejs/plugin-react": "^6.0.0", "autoprefixer": "^10.4.18", "concurrently": "^8.2.0", + "electron": "^31.0.0", + "electron-builder": "^24.13.3", "eslint": "^9.9.1", "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.11", @@ -730,6 +732,269 @@ "node": ">=18" } }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@electron/notarize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.2.1.tgz", + "integrity": "sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.0.5.tgz", + "integrity": "sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/universal": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.5.1.tgz", + "integrity": "sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.1", + "@malept/cross-spawn-promise": "^1.1.0", + "debug": "^4.3.1", + "dir-compare": "^3.0.0", + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/@emnapi/core": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", @@ -987,6 +1252,109 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -1074,6 +1442,84 @@ "node": ">=6.0.0" } }, + "node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", @@ -1103,6 +1549,17 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@remix-run/router": { "version": "1.23.2", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", @@ -1766,6 +2223,32 @@ "dev": true, "license": "MIT" }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@tailwindcss/typography": { "version": "0.5.19", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", @@ -1869,6 +2352,16 @@ "react-dom": "^18.0.0" } }, + "node_modules/@tootallnate/once": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", + "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -1887,6 +2380,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -1911,6 +2417,16 @@ "@types/estree": "*" } }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -1920,6 +2436,13 @@ "@types/unist": "*" } }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "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", @@ -1927,6 +2450,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -1942,6 +2475,28 @@ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "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/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -1974,12 +2529,41 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/parser": { "version": "8.59.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz", @@ -3808,6 +4392,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", + "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -3871,6 +4472,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, "node_modules/ajv/node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -3928,6 +4539,204 @@ "node": ">= 8" } }, + "node_modules/app-builder-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz", + "integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.13.3.tgz", + "integrity": "sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/notarize": "2.2.1", + "@electron/osx-sign": "1.0.5", + "@electron/universal": "1.5.1", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "bluebird-lst": "^1.0.9", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chromium-pickle-js": "^0.2.0", + "debug": "^4.3.4", + "ejs": "^3.1.8", + "electron-publish": "24.13.1", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^5.0.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "minimatch": "^5.1.1", + "read-config-file": "6.3.2", + "sanitize-filename": "^1.6.3", + "semver": "^7.3.8", + "tar": "^6.1.12", + "temp-file": "^3.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "24.13.3", + "electron-builder-squirrel-windows": "24.13.3" + } + }, + "node_modules/app-builder-lib/node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -3969,6 +4778,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -3979,6 +4799,34 @@ "node": "*" } }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3986,6 +4834,16 @@ "dev": true, "license": "MIT" }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/autoprefixer": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", @@ -4056,6 +4914,27 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.20", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", @@ -4082,27 +4961,66 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.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==", + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/bluebird-lst": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", + "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", "dev": true, "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { + "bluebird": "^3.5.5" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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" } }, @@ -4159,13 +5077,179 @@ "browserslist": "*" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", + "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "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/builder-util": { + "version": "24.13.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.13.1.tgz", + "integrity": "sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==", + "dev": true, "license": "MIT", - "peer": true + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "4.0.0", + "bluebird-lst": "^1.0.9", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-ci": "^3.0.0", + "js-yaml": "^4.1.0", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/builder-util/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } }, "node_modules/cac": { "version": "6.7.14", @@ -4177,6 +5261,51 @@ "node": ">=8" } }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -4418,6 +5547,70 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4471,6 +5664,33 @@ "node": ">= 6" } }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4530,6 +5750,75 @@ "dev": true, "license": "MIT" }, + "node_modules/config-file-ts": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.6.tgz", + "integrity": "sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.3.10", + "typescript": "^5.3.3" + } + }, + "node_modules/config-file-ts/node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/config-file-ts/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -4551,7 +5840,54 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/cross-spawn": { + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", @@ -4683,6 +6019,35 @@ "node": ">=14.16" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-eql": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", @@ -4736,6 +6101,16 @@ "dev": true, "license": "MIT" }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -4743,121 +6118,456 @@ "dev": true, "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-compare": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz", + "integrity": "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal": "^1.0.0", + "minimatch": "^3.0.4" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dmg-builder": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.13.3.tgz", + "integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "24.13.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", + "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "31.7.7", + "resolved": "https://registry.npmjs.org/electron/-/electron-31.7.7.tgz", + "integrity": "sha512-HZtZg8EHsDGnswFt0QeV8If8B+et63uD6RJ7I4/xhcXqmTIbI08GoubX/wm+HdY0DwcuPe1/xsgqpmYvjdjRoA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^20.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.13.3.tgz", + "integrity": "sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "24.13.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "dmg-builder": "24.13.3", + "fs-extra": "^10.1.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "read-config-file": "6.3.2", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-24.13.3.tgz", + "integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "24.13.3", + "archiver": "^5.3.1", + "builder-util": "24.13.1", + "fs-extra": "^10.1.0" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=12" } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" + "universalify": "^2.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/electron-builder-squirrel-windows/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", + "peer": true, "engines": { - "node": ">=0.4.0" + "node": ">= 10.0.0" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, "engines": { - "node": ">=6" + "node": ">=12" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", "license": "MIT", "dependencies": { - "dequal": "^2.0.0" + "universalify": "^2.0.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 10.0.0" } }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "node_modules/electron-publish": { + "version": "24.13.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.13.1.tgz", + "integrity": "sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } }, - "node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" + "universalify": "^2.0.0" }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">= 10.0.0" } }, "node_modules/electron-to-chromium": { @@ -4874,6 +6584,16 @@ "dev": true, "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -4886,6 +6606,23 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -4956,6 +6693,14 @@ "node": ">= 0.4" } }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -5206,6 +6951,54 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5305,6 +7098,16 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -5318,6 +7121,39 @@ "node": ">=16.0.0" } }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -5386,47 +7222,137 @@ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, - "license": "MIT", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", "dependencies": { - "is-callable": "^1.2.7" + "minipass": "^3.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 8" } }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "yallist": "^4.0.0" }, "engines": { - "node": ">= 6" + "node": ">=8" } }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", @@ -5545,6 +7471,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -5558,6 +7506,25 @@ "node": ">=10.13.0" } }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, "node_modules/globals": { "version": "15.15.0", "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", @@ -5571,6 +7538,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -5584,6 +7569,39 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "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-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -5832,6 +7850,39 @@ "node": ">=12.0.0" } }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -5865,6 +7916,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -5879,6 +7937,20 @@ "node": ">= 14" } }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -5903,6 +7975,24 @@ "node": ">=16.17.0" } }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -5916,6 +8006,27 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -5963,6 +8074,25 @@ "node": ">=8" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, "node_modules/inline-style-parser": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", @@ -6102,6 +8232,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -6380,6 +8523,19 @@ "dev": true, "license": "MIT" }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -6387,6 +8543,40 @@ "dev": true, "license": "ISC" }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -6491,6 +8681,14 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -6504,6 +8702,16 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -6514,6 +8722,71 @@ "json-buffer": "3.0.1" } }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -6866,7 +9139,39 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true, - "license": "MIT" + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT", + "peer": true }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -6875,6 +9180,14 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -6907,6 +9220,16 @@ "get-func-name": "^2.0.1" } }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -6956,6 +9279,20 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -7900,6 +10237,19 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -7936,6 +10286,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -7959,6 +10319,73 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mlly": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", @@ -8023,6 +10450,14 @@ "dev": true, "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/node-releases": { "version": "2.0.37", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", @@ -8040,6 +10475,19 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -8157,6 +10605,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/onetime": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", @@ -8191,6 +10649,16 @@ "node": ">= 0.8.0" } }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -8223,6 +10691,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8258,6 +10733,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -8275,6 +10760,30 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -8292,6 +10801,13 @@ "node": "*" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -8351,6 +10867,31 @@ "dev": true, "license": "MIT" }, + "node_modules/plist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz", + "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.9.10", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/plist/node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -8562,6 +11103,38 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -8585,6 +11158,17 @@ "url": "https://github.com/sponsors/lupomontero" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -8640,6 +11224,19 @@ ], "license": "MIT" }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -8741,6 +11338,76 @@ "pify": "^2.3.0" } }, + "node_modules/read-config-file": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz", + "integrity": "sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-file-ts": "^0.2.4", + "dotenv": "^9.0.2", + "dotenv-expand": "^5.1.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.0", + "lazy-val": "^1.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -9003,6 +11670,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -9013,6 +11687,29 @@ "node": ">=4" } }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -9024,6 +11721,25 @@ "node": ">=0.10.0" } }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/rolldown": { "version": "1.0.0-rc.16", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz", @@ -9168,6 +11884,28 @@ "tslib": "^2.1.0" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, "node_modules/safe-regex-test": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", @@ -9193,6 +11931,16 @@ "dev": true, "license": "MIT" }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, "node_modules/sax": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", @@ -9238,6 +11986,31 @@ "node": ">=10" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -9404,13 +12177,53 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, "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", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9431,7 +12244,6 @@ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -9465,6 +12277,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -9472,6 +12292,16 @@ "dev": true, "license": "MIT" }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", @@ -9493,6 +12323,17 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -9508,6 +12349,22 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -9535,6 +12392,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -9646,6 +12517,19 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -9727,6 +12611,99 @@ "node": ">=14.0.0" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/terser": { "version": "5.46.1", "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", @@ -9853,6 +12830,26 @@ "node": ">=14.0.0" } }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -9925,6 +12922,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -9975,6 +12982,20 @@ "node": ">=4" } }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -10238,6 +13259,13 @@ "dev": true, "license": "MIT" }, + "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/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", @@ -10430,6 +13458,13 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -10437,6 +13472,22 @@ "dev": true, "license": "MIT" }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -11832,6 +14883,32 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, "node_modules/ws": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", @@ -11956,6 +15033,17 @@ "node": ">=10" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -11969,6 +15057,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/zustand": { "version": "4.5.7", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", From 6f15d08fe4a867e1ac8ca4f1b01aac41ff9b8e75 Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Mon, 13 Jul 2026 17:52:09 +0800 Subject: [PATCH 08/11] fix(storage): prevent data loss on migration/quit (prefer richest snapshot, unload-safe mirror) --- src/services/indexedDbStorage.ts | 126 +++++++++++++++++++++++++++---- 1 file changed, 110 insertions(+), 16 deletions(-) diff --git a/src/services/indexedDbStorage.ts b/src/services/indexedDbStorage.ts index 0525b554..e0e9933b 100644 --- a/src/services/indexedDbStorage.ts +++ b/src/services/indexedDbStorage.ts @@ -106,13 +106,83 @@ const idbDelete = async (key: string): Promise => { }); }; +/** + * Snapshot helpers — used to pick the *richest* available copy so a stale or + * empty IndexedDB value can never shadow good data held in localStorage (and + * vice-versa). Persistence must never silently drop historical user data. + */ +interface PersistSnapshot { + state?: Record; + version?: number; +} + +const parseSnapshot = (raw: string | null): PersistSnapshot | null => { + if (!raw) return null; + try { + return JSON.parse(raw) as PersistSnapshot; + } catch { + return null; + } +}; + +const isNonEmptySnapshot = (snap: PersistSnapshot | null): boolean => { + if (!snap || !snap.state) return false; + const st = snap.state; + const repos = Array.isArray(st.repositories) ? (st.repositories as unknown[]).length : 0; + const gists = Array.isArray(st.gists) ? (st.gists as unknown[]).length : 0; + const starred = Array.isArray(st.starredGists) ? (st.starredGists as unknown[]).length : 0; + return !!st.user || repos > 0 || gists > 0 || starred > 0; +}; + +const snapshotRichness = (snap: PersistSnapshot | null): number => { + if (!snap || !snap.state) return 0; + const st = snap.state; + const repos = Array.isArray(st.repositories) ? (st.repositories as unknown[]).length : 0; + const gists = Array.isArray(st.gists) ? (st.gists as unknown[]).length : 0; + const starred = Array.isArray(st.starredGists) ? (st.starredGists as unknown[]).length : 0; + return repos + gists + starred + (st.user ? 1 : 0); +}; + +// Cache the latest value so we can mirror it to localStorage on page unload, +// where an async IndexedDB write would never complete. +let latestValue: { name: string; value: string } | null = null; +let flushListenersRegistered = false; + +const registerFlushListeners = (): void => { + if (flushListenersRegistered || typeof window === 'undefined') return; + flushListenersRegistered = true; + + const flush = (): void => { + if (latestValue) { + // localStorage writes are synchronous and survive page teardown, making + // them a reliable unload-safe mirror of the most recent state. + safeLocalStorageSet(latestValue.name, latestValue.value); + } + }; + + window.addEventListener('pagehide', flush); + window.addEventListener('beforeunload', flush); + + if (typeof document !== 'undefined') { + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'hidden') { + flush(); + } + }); + } +}; + /** * IndexedDB-backed Zustand persist storage with seamless migration: - * - First read from IndexedDB - * - If empty, migrate an existing localStorage snapshot to IndexedDB and then remove it - * - Normal writes go to IndexedDB and clear any legacy localStorage snapshot. - * - localStorage is only kept as the current snapshot when IndexedDB is unavailable or a write fails. - * This avoids stale fallback rollbacks while preserving persistence in constrained environments. + * - Reads from IndexedDB and localStorage, preferring whichever copy holds the + * richest (non-empty) snapshot — a stale/empty IndexedDB value can no + * longer shadow good data held in the other store. + * - On read, if localStorage holds the only good copy it is migrated into + * IndexedDB so the two stay consistent. + * - Writes go to IndexedDB (large-data friendly) and a localStorage mirror is + * kept as an unload-safe fallback. The only good copy is never destroyed. + * - localStorage is also used as the sole store when IndexedDB is unavailable + * or a write fails, so persistence works in constrained environments. */ export const indexedDBStorage: StateStorage = { getItem: async (name: string): Promise => { @@ -124,17 +194,37 @@ export const indexedDBStorage: StateStorage = { } try { - const idbValue = await withTimeout(idbGet(name)); - if (idbValue !== null) return idbValue; - - // Migration path: restore existing localStorage snapshot into IndexedDB - const legacyValue = safeLocalStorageGet(name); - if (legacyValue !== null) { - await withTimeout(idbSet(name, legacyValue)); - safeLocalStorageRemove(name); - console.info('[storage] migrated state from localStorage to IndexedDB'); + const idbRaw = await withTimeout(idbGet(name)); + const lsRaw = safeLocalStorageGet(name); + const idbSnap = parseSnapshot(idbRaw); + const lsSnap = parseSnapshot(lsRaw); + const idbOk = isNonEmptySnapshot(idbSnap); + const lsOk = isNonEmptySnapshot(lsSnap); + + let chosen: string | null; + if (idbOk && lsOk) { + // Both have data: prefer the richer copy to avoid stale rollback. + chosen = snapshotRichness(idbSnap) >= snapshotRichness(lsSnap) ? idbRaw : lsRaw; + } else if (idbOk) { + chosen = idbRaw; + } else if (lsOk) { + chosen = lsRaw; + } else { + // Neither has meaningful data; return whatever exists. + chosen = idbRaw ?? lsRaw; + } + + // If localStorage held the only good copy, migrate it into IndexedDB. + if (lsOk && !idbOk && lsRaw) { + try { + await withTimeout(idbSet(name, lsRaw)); + console.info('[storage] migrated state from localStorage to IndexedDB'); + } catch (error) { + console.warn('[storage] IndexedDB migration write failed, keeping localStorage', error); + } } - return legacyValue; + + return chosen; } catch (error) { console.warn('[storage] IndexedDB get failed, fallback to localStorage:', error); return safeLocalStorageGet(name); @@ -144,17 +234,20 @@ export const indexedDBStorage: StateStorage = { setItem: async (name: string, value: string): Promise => { if (typeof window === 'undefined') return; + latestValue = { name, value }; + registerFlushListeners(); + // Primary path: IndexedDB first (large data friendly) if (canUseIndexedDB()) { try { await withTimeout(idbSet(name, value)); - safeLocalStorageRemove(name); return; } catch (error) { console.warn('[storage] IndexedDB set failed, fallback to localStorage:', error); } } + // Keep a localStorage mirror as a reliable, unload-safe copy. if (!safeLocalStorageSet(name, value)) { throw new Error('[storage] localStorage fallback write failed'); } @@ -163,6 +256,7 @@ export const indexedDBStorage: StateStorage = { removeItem: async (name: string): Promise => { if (typeof window === 'undefined') return; + latestValue = null; safeLocalStorageRemove(name); if (!canUseIndexedDB()) return; From fdf1a0327dc5a0ed2da74fb8d956b577957ff649 Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Mon, 13 Jul 2026 17:59:49 +0800 Subject: [PATCH 09/11] fix(storage): use write-timestamp freshness instead of collection size (CodeRabbit r3569671054) --- src/services/indexedDbStorage.ts | 119 +++++++++++++++++++++++-------- 1 file changed, 89 insertions(+), 30 deletions(-) diff --git a/src/services/indexedDbStorage.ts b/src/services/indexedDbStorage.ts index e0e9933b..adcbbb75 100644 --- a/src/services/indexedDbStorage.ts +++ b/src/services/indexedDbStorage.ts @@ -4,6 +4,15 @@ export const DB_NAME = 'github-stars-manager-db'; const STORE_NAME = 'app_state'; const DB_VERSION = 1; +// Freshness metadata embedded alongside every persisted snapshot so we can pick +// the *newest* valid copy regardless of collection size (a newer snapshot may +// legitimately contain fewer repositories after intentional deletions, and equal +// sizes can hide newer edits to settings/tags/metadata). +const META_KEY = '__gsm_meta'; +interface SnapshotMeta { + ts: number; +} + const canUseIndexedDB = () => typeof window !== 'undefined' && typeof window.indexedDB !== 'undefined'; const withTimeout = async (promise: Promise, timeoutMs = 2000): Promise => { @@ -107,15 +116,35 @@ const idbDelete = async (key: string): Promise => { }; /** - * Snapshot helpers — used to pick the *richest* available copy so a stale or - * empty IndexedDB value can never shadow good data held in localStorage (and - * vice-versa). Persistence must never silently drop historical user data. + * Snapshot helpers — used to pick the *newest valid* available copy so a + * stale snapshot can never shadow good data, and to repair divergence between + * IndexedDB and localStorage. */ interface PersistSnapshot { state?: Record; version?: number; + [META_KEY]?: SnapshotMeta; } +const normalizeValue = (value: unknown): string => { + if (typeof value === 'string') return value; + try { + return JSON.stringify(value ?? null); + } catch { + return 'null'; + } +}; + +const stamp = (raw: string): string => { + try { + const obj = JSON.parse(raw) as Record; + obj[META_KEY] = { ts: Date.now() } as SnapshotMeta; + return JSON.stringify(obj); + } catch { + return raw; + } +}; + const parseSnapshot = (raw: string | null): PersistSnapshot | null => { if (!raw) return null; try { @@ -174,15 +203,19 @@ const registerFlushListeners = (): void => { /** * IndexedDB-backed Zustand persist storage with seamless migration: - * - Reads from IndexedDB and localStorage, preferring whichever copy holds the - * richest (non-empty) snapshot — a stale/empty IndexedDB value can no - * longer shadow good data held in the other store. - * - On read, if localStorage holds the only good copy it is migrated into - * IndexedDB so the two stay consistent. - * - Writes go to IndexedDB (large-data friendly) and a localStorage mirror is - * kept as an unload-safe fallback. The only good copy is never destroyed. - * - localStorage is also used as the sole store when IndexedDB is unavailable - * or a write fails, so persistence works in constrained environments. + * - Reads from IndexedDB and localStorage, selecting the *newest valid* + * snapshot via embedded write timestamps — never by collection size, so a + * stale (but larger) copy can no longer be restored; if neither copy + * carries freshness metadata (legacy recovery), richness is the fallback. + * - After selecting, the divergent/older store is repaired from the chosen + * snapshot so the two copies stay consistent. + * - Writes go to IndexedDB (large-data friendly) and a localStorage mirror + * carrying the same timestamp; the mirror is best-effort (quota-limited) + * and used as an unload-safe fallback. The only good copy is never + * destroyed. + * - localStorage is also used as the sole store when IndexedDB is + * unavailable or a write fails, so persistence works in constrained + * environments. */ export const indexedDBStorage: StateStorage = { getItem: async (name: string): Promise => { @@ -201,30 +234,51 @@ export const indexedDBStorage: StateStorage = { const idbOk = isNonEmptySnapshot(idbSnap); const lsOk = isNonEmptySnapshot(lsSnap); - let chosen: string | null; + let selected: string | null; if (idbOk && lsOk) { - // Both have data: prefer the richer copy to avoid stale rollback. - chosen = snapshotRichness(idbSnap) >= snapshotRichness(lsSnap) ? idbRaw : lsRaw; + const idbTs = idbSnap?.[META_KEY]?.ts; + const lsTs = lsSnap?.[META_KEY]?.ts; + if (typeof idbTs === 'number' && typeof lsTs === 'number') { + // Both carry freshness metadata: pick the newest; tie-break by richness. + selected = + idbTs > lsTs || (idbTs === lsTs && snapshotRichness(idbSnap) >= snapshotRichness(lsSnap)) + ? idbRaw + : lsRaw; + } else if (typeof idbTs === 'number') { + // Only IndexedDB has metadata (current build) → it is the newer copy. + selected = idbRaw; + } else if (typeof lsTs === 'number') { + selected = lsRaw; + } else { + // Legacy recovery: neither carries metadata, fall back to richness. + selected = snapshotRichness(idbSnap) >= snapshotRichness(lsSnap) ? idbRaw : lsRaw; + } } else if (idbOk) { - chosen = idbRaw; + selected = idbRaw; } else if (lsOk) { - chosen = lsRaw; + selected = lsRaw; } else { // Neither has meaningful data; return whatever exists. - chosen = idbRaw ?? lsRaw; + selected = idbRaw ?? lsRaw; } - // If localStorage held the only good copy, migrate it into IndexedDB. - if (lsOk && !idbOk && lsRaw) { - try { - await withTimeout(idbSet(name, lsRaw)); - console.info('[storage] migrated state from localStorage to IndexedDB'); - } catch (error) { - console.warn('[storage] IndexedDB migration write failed, keeping localStorage', error); + // Repair the divergent/older store from the selected snapshot so the two + // copies converge (idempotent; skipped silently on failure). + if (selected) { + if (selected === idbRaw && selected !== lsRaw) { + if (!safeLocalStorageSet(name, selected)) { + console.warn('[storage] localStorage repair write failed (quota?)'); + } + } else if (selected === lsRaw && selected !== idbRaw) { + try { + await withTimeout(idbSet(name, stamp(selected))); + } catch (error) { + console.warn('[storage] IndexedDB repair write failed', error); + } } } - return chosen; + return selected; } catch (error) { console.warn('[storage] IndexedDB get failed, fallback to localStorage:', error); return safeLocalStorageGet(name); @@ -234,21 +288,26 @@ export const indexedDBStorage: StateStorage = { setItem: async (name: string, value: string): Promise => { if (typeof window === 'undefined') return; - latestValue = { name, value }; + const stamped = stamp(normalizeValue(value)); + latestValue = { name, value: stamped }; registerFlushListeners(); // Primary path: IndexedDB first (large data friendly) if (canUseIndexedDB()) { try { - await withTimeout(idbSet(name, value)); + await withTimeout(idbSet(name, stamped)); + // Best-effort mirror so localStorage also carries freshness metadata and + // can be used as an unload-safe fallback. Quota failures are non-fatal. + if (!safeLocalStorageSet(name, stamped)) { + console.warn('[storage] localStorage mirror write failed (quota?)'); + } return; } catch (error) { console.warn('[storage] IndexedDB set failed, fallback to localStorage:', error); } } - // Keep a localStorage mirror as a reliable, unload-safe copy. - if (!safeLocalStorageSet(name, value)) { + if (!safeLocalStorageSet(name, stamped)) { throw new Error('[storage] localStorage fallback write failed'); } }, From d09797dcd3295c460d4e9bfbbf3f7c8f8a906f1d Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Mon, 13 Jul 2026 18:57:14 +0800 Subject: [PATCH 10/11] =?UTF-8?q?fix(storage):=20harden=20against=20data?= =?UTF-8?q?=20loss=20=E2=80=94=20gate=20sync=20on=20hydration=20+=20empty-?= =?UTF-8?q?write=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: zustand v4 hydration is async and races with syncFromBackend() (called in App.tsx useEffect). Before hydration finishes, state.repositories is the empty initial value, which defeats the isBootstrapEmpty guard in autoSync and lets an empty setRepositories() overwrite good IndexedDB data on the next persist write (the 1s debounce makes the empty write land after the good-data write). This is a pre-existing race on main; this build was the one that hit the window. Fixes: - F1 (root cause): App.tsx waits for hasHydrated before calling syncFromBackend(); autoSync.syncFromBackend() early-returns if !hasHydrated (defense in depth). - F2 (backstop): indexedDbStorage.setItem refuses to overwrite a non-empty snapshot with an empty/invalid one. - F3 (durability): latestValue + pagehide/visibilitychange synchronous localStorage mirror already keeps the freshest value safe across teardown. - F4 (regression lock): tests for the empty-write guard and the sync hydration gate. Verified: tsc --noEmit clean, eslint clean, full vitest 114/114. --- src/App.tsx | 24 +++++++++- src/services/autoSync.test.ts | 32 +++++++++++++ src/services/autoSync.ts | 8 ++++ src/services/indexedDbStorage.test.ts | 67 +++++++++++++++++++++++++++ src/services/indexedDbStorage.ts | 25 ++++++++++ 5 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/services/autoSync.test.ts create mode 100644 src/services/indexedDbStorage.test.ts diff --git a/src/App.tsx b/src/App.tsx index 6945f14b..fbf78d55 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -142,12 +142,34 @@ function App() { let unsubscribe: (() => void) | null = null; let cancelled = false; + // Wait until zustand persist has finished hydrating from IndexedDB. + // Sync must never run against the empty *initial* state — doing so defeats + // the isBootstrapEmpty guard in autoSync and overwrites good local data + // with an empty snapshot (the data-loss incident root cause). + const waitForHydration = (): Promise => + new Promise((resolve) => { + if (useAppStore.getState().hasHydrated) { + resolve(); + return; + } + const unsubscribe = useAppStore.subscribe((state) => { + if (state.hasHydrated) { + unsubscribe(); + resolve(); + } + }); + }); + const initBackend = async () => { try { await backend.init(); initMcpRendererBridge(); if (backend.isAvailable && !cancelled) { - await syncFromBackend(); + // Block on hydration so the first pull never reads the empty initial state. + await waitForHydration(); + if (!cancelled) { + await syncFromBackend(); + } if (!cancelled) { unsubscribe = startAutoSync(); } diff --git a/src/services/autoSync.test.ts b/src/services/autoSync.test.ts new file mode 100644 index 00000000..ba98db28 --- /dev/null +++ b/src/services/autoSync.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('../services/backendAdapter', () => ({ + backend: { + isAvailable: true, + fetchRepositories: vi.fn(), + fetchReleases: vi.fn(), + fetchAIConfigs: vi.fn(), + fetchWebDAVConfigs: vi.fn(), + fetchEmbeddingConfigs: vi.fn(), + fetchVectorSearchConfig: vi.fn(), + fetchMcpConfig: vi.fn(), + fetchSettings: vi.fn(), + }, +})); + +vi.mock('../store/useAppStore', () => ({ + useAppStore: { + getState: () => ({ hasHydrated: false }), + subscribe: () => () => {}, + }, +})); + +import { syncFromBackend } from './autoSync'; +import { backend } from '../services/backendAdapter'; + +describe('syncFromBackend hydration gate', () => { + it('returns early before hydration (prevents empty-overwrite race)', async () => { + await syncFromBackend(); + expect(backend.fetchRepositories).not.toHaveBeenCalled(); + }); +}); diff --git a/src/services/autoSync.ts b/src/services/autoSync.ts index d4450e5e..762a07a6 100644 --- a/src/services/autoSync.ts +++ b/src/services/autoSync.ts @@ -54,6 +54,14 @@ function setRepositorySyncVisualState(isSyncing: boolean): void { * Silent: errors logged to console only. */ export async function syncFromBackend(): Promise { + // Defense in depth: never pull from backend before persist hydration + // completes. Before hydration, state.repositories is the empty initial + // value, which defeats the isBootstrapEmpty guard and would overwrite + // good local data with an empty snapshot (data-loss root cause). + if (!useAppStore.getState().hasHydrated) { + return; + } + if ( !backend.isAvailable || _isSyncingFromBackendActive || diff --git a/src/services/indexedDbStorage.test.ts b/src/services/indexedDbStorage.test.ts new file mode 100644 index 00000000..f63a72fb --- /dev/null +++ b/src/services/indexedDbStorage.test.ts @@ -0,0 +1,67 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { indexedDBStorage } from './indexedDbStorage'; + +const NAME = 'github-stars-manager'; + +// In-memory localStorage mock: the test node environment does not provision +// jsdom's localStorage, and safeLocalStorage* swallows the resulting errors. +const mem = new Map(); +const mockLocalStorage = { + getItem: (k: string) => (mem.has(k) ? mem.get(k)! : null), + setItem: (k: string, v: string) => { + mem.set(k, String(v)); + }, + removeItem: (k: string) => { + mem.delete(k); + }, + clear: () => { + mem.clear(); + }, +}; + +const nonEmpty = JSON.stringify({ + state: { user: { login: 'me' }, repositories: [{ id: 1 }], gists: [], starredGists: [] }, + version: 9, +}); + +const empty = JSON.stringify({ + state: { user: null, repositories: [], gists: [], starredGists: [] }, + version: 9, +}); + +const readState = async (): Promise<{ repositories: unknown[] } | null> => { + const raw = await indexedDBStorage.getItem(NAME); + return raw ? (JSON.parse(raw).state as { repositories: unknown[] }) : null; +}; + +describe('indexedDBStorage empty-write guard', () => { + beforeEach(() => { + // Force the localStorage-only path so we can test without fake-indexeddb. + (window as unknown as { indexedDB?: unknown }).indexedDB = undefined; + (window as unknown as { localStorage?: unknown }).localStorage = mockLocalStorage; + mem.clear(); + }); + + it('refuses to overwrite a non-empty snapshot with an empty one', async () => { + await indexedDBStorage.setItem(NAME, nonEmpty); + await indexedDBStorage.setItem(NAME, empty); + const after = await readState(); + expect(after).not.toBeNull(); + expect(after!.repositories.length).toBeGreaterThan(0); + }); + + it('allows the first write even when it is empty (no existing data)', async () => { + await indexedDBStorage.setItem(NAME, empty); + expect(await readState()).not.toBeNull(); + }); + + it('allows overwriting non-empty with a richer non-empty snapshot', async () => { + await indexedDBStorage.setItem(NAME, nonEmpty); + const richer = JSON.stringify({ + state: { user: { login: 'me' }, repositories: [1, 2, 3].map((id) => ({ id })), gists: [], starredGists: [] }, + version: 9, + }); + await indexedDBStorage.setItem(NAME, richer); + expect((await readState())!.repositories.length).toBe(3); + }); +}); diff --git a/src/services/indexedDbStorage.ts b/src/services/indexedDbStorage.ts index adcbbb75..0d0396f0 100644 --- a/src/services/indexedDbStorage.ts +++ b/src/services/indexedDbStorage.ts @@ -292,6 +292,31 @@ export const indexedDBStorage: StateStorage = { latestValue = { name, value: stamped }; registerFlushListeners(); + // Defensive gate: never let an empty/invalid snapshot clobber a + // non-empty one. A failed hydration (migrate/merge error or an + // unreadable snapshot) otherwise writes the initial empty state over + // good data and wipes the user's entire history in one write. + if (!isNonEmptySnapshot(parseSnapshot(stamped))) { + try { + const [idbRaw, lsRaw] = await Promise.all([ + canUseIndexedDB() ? withTimeout(idbGet(name)).catch(() => null) : Promise.resolve(null), + Promise.resolve(safeLocalStorageGet(name)), + ]); + const existingOk = + isNonEmptySnapshot(parseSnapshot(idbRaw)) || + isNonEmptySnapshot(parseSnapshot(lsRaw)); + if (existingOk) { + console.warn('[storage] refusing to overwrite non-empty data with an empty snapshot'); + return; + } + } catch { + // If we cannot verify the existing copy, refuse the empty write + // rather than risk clobbering good data. + console.warn('[storage] refusing empty write (cannot verify existing data)'); + return; + } + } + // Primary path: IndexedDB first (large data friendly) if (canUseIndexedDB()) { try { From 680b0f1fea3347cbd471d03afff3c5bf6d393063 Mon Sep 17 00:00:00 2001 From: AmintaCCCP Date: Mon, 13 Jul 2026 19:30:09 +0800 Subject: [PATCH 11/11] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20PR#247=20?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E5=89=A9=E4=BD=99=20CodeRabbit=20=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - indexedDbStorage: 修复巡检(repair)保留原时间戳;空写闸门拒绝后不再缓存 latestValue,避免 pagehide/visibilitychange 绕过闸门写入空快照 - server mcp: 每请求/连接创建独立 McpServer;loadMcpState 与鉴权移入 try;语义搜索失败显式抛出而非返回空结果;tools 暴露 offset 并将 limit/topK 约束为整数 - configs: MCP 默认端口统一为 18789;启用 MCP 但缺少 token 时拒绝(400) - backendAdapter: MCP token 拉取请求标记为敏感,debug 日志脱敏 - mcpRendererBridge: 语义搜索失败抛出;禁用/改端口时正确停止或重启内嵌服务 - useAppStore: 快照不再持久化 MCP bearer token 明文 - schema: mcp_configs 端口默认值 18789 --- scripts/build-desktop.js | 13 ++++-- server/src/db/schema.ts | 2 +- server/src/mcp/index.ts | 66 +++++++++++++++---------------- server/src/mcp/sqliteProvider.ts | 28 +++++++++---- server/src/mcp/tools.ts | 8 ++-- server/src/routes/configs.ts | 12 +++++- src/services/backendAdapter.ts | 25 ++++++++++-- src/services/indexedDbStorage.ts | 13 ++++-- src/services/mcpRendererBridge.ts | 31 ++++++++++++--- src/store/useAppStore.ts | 5 ++- 10 files changed, 139 insertions(+), 64 deletions(-) diff --git a/scripts/build-desktop.js b/scripts/build-desktop.js index 7ddc40bb..b9c6a226 100644 --- a/scripts/build-desktop.js +++ b/scripts/build-desktop.js @@ -359,14 +359,20 @@ function getStats() { return { totalRepositories: repos.length, analyzedCount: repos.filter((r) => r.analyzed).length, byLanguage, byCategory, topTags: topTags().slice(0, 20) }; } +function buildServer() { + // A fresh McpServer per request/connection: connect() owns the transport + // and only supports one active connection, so a shared instance can break + // the second client. + const s = new McpServer({ name: 'github-stars-manager', version: '0.7.0' }, { capabilities: { tools: {} } }); + registerTools(s); + return s; +} + async function startMcp({ port, token }) { authToken = token || ''; if (!authToken) throw new Error('MCP token is required'); if (httpServer) await stopMcp(); - const server = new McpServer({ name: 'github-stars-manager', version: '0.7.0' }, { capabilities: { tools: {} } }); - registerTools(server); - const listenPort = port && Number.isInteger(port) && port > 0 ? port : 18789; const httpServerInstance = http.createServer(async (req, res) => { @@ -416,6 +422,7 @@ async function startMcp({ port, token }) { } try { + const server = buildServer(); if (pathname === '/mcp') { const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); res.on('close', () => transport.close().catch(() => undefined)); diff --git a/server/src/db/schema.ts b/server/src/db/schema.ts index ffb237cf..4cb046db 100644 --- a/server/src/db/schema.ts +++ b/server/src/db/schema.ts @@ -138,7 +138,7 @@ export function initializeSchema(db: Database.Database): void { CREATE TABLE IF NOT EXISTS mcp_configs ( id TEXT PRIMARY KEY DEFAULT 'default', enabled INTEGER NOT NULL DEFAULT 0, - port INTEGER NOT NULL DEFAULT 0, + port INTEGER NOT NULL DEFAULT 18789, token_encrypted TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) diff --git a/server/src/mcp/index.ts b/server/src/mcp/index.ts index 9fadf888..88d04b5f 100644 --- a/server/src/mcp/index.ts +++ b/server/src/mcp/index.ts @@ -88,16 +88,16 @@ export function initMcp(app: Express): void { // ── Streamable HTTP (primary, MCP 2025 spec) ── app.all('/mcp', async (req, res) => { - const state = loadMcpState(); - if (!state) { - res.status(404).json({ error: 'not found' }); - return; - } - if (!tokenMatches(extractBearer(req), state.token)) { - unauthorized(res); - return; - } try { + const state = loadMcpState(); + if (!state) { + res.status(404).json({ error: 'not found' }); + return; + } + if (!tokenMatches(extractBearer(req), state.token)) { + unauthorized(res); + return; + } const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); res.on('close', () => { transport.close().catch(() => undefined); @@ -113,16 +113,16 @@ export function initMcp(app: Express): void { // ── SSE fallback (for clients not yet supporting Streamable HTTP) ── app.get('/mcp/sse', async (req, res) => { - const state = loadMcpState(); - if (!state) { - res.status(404).json({ error: 'not found' }); - return; - } - if (!tokenMatches(extractBearer(req), state.token)) { - unauthorized(res); - return; - } try { + const state = loadMcpState(); + if (!state) { + res.status(404).json({ error: 'not found' }); + return; + } + if (!tokenMatches(extractBearer(req), state.token)) { + unauthorized(res); + return; + } const transport = new SSEServerTransport('/mcp/sse/messages', res); sseTransports.set(transport.sessionId, transport); res.on('close', () => { @@ -137,22 +137,22 @@ export function initMcp(app: Express): void { }); app.post('/mcp/sse/messages', async (req, res) => { - const state = loadMcpState(); - if (!state) { - res.status(404).json({ error: 'not found' }); - return; - } - if (!tokenMatches(extractBearer(req), state.token)) { - unauthorized(res); - return; - } - const sessionId = req.query.sessionId as string | undefined; - const transport = sessionId ? sseTransports.get(sessionId) : undefined; - if (!transport) { - res.status(404).json({ error: 'unknown session' }); - return; - } try { + const state = loadMcpState(); + if (!state) { + res.status(404).json({ error: 'not found' }); + return; + } + if (!tokenMatches(extractBearer(req), state.token)) { + unauthorized(res); + return; + } + const sessionId = req.query.sessionId as string | undefined; + const transport = sessionId ? sseTransports.get(sessionId) : undefined; + if (!transport) { + res.status(404).json({ error: 'unknown session' }); + return; + } await transport.handlePostMessage(req, res, req.body); } catch (err) { logger.errorFromError('mcp.sseMsg', 'SSE message error', err as Error); diff --git a/server/src/mcp/sqliteProvider.ts b/server/src/mcp/sqliteProvider.ts index 62f61186..3b9017b1 100644 --- a/server/src/mcp/sqliteProvider.ts +++ b/server/src/mcp/sqliteProvider.ts @@ -2,6 +2,7 @@ import type Database from 'better-sqlite3'; import axios from 'axios'; import { decrypt } from '../services/crypto.js'; import { config } from '../config.js'; +import { logger } from '../services/logger.js'; import type { McpDataProvider, McpListFilter, @@ -169,20 +170,25 @@ export class SqliteMcpProvider implements McpDataProvider { async semanticSearch(query: string, topK: number): Promise> { const v = this.readVectorConfig(); if (!v || !v.enabled || !v.workerUrl || !v.embeddingConfigId) { + // Vector search not configured — not a failure; surface as an empty result. return []; } const embRow = this.db .prepare('SELECT * FROM embedding_configs WHERE id = ?') .get(v.embeddingConfigId) as Record | undefined; - if (!embRow) return []; + if (!embRow) { + // No embedding configuration — not a failure; surface as an empty result. + return []; + } let apiKey = ''; if (embRow.api_key_encrypted) { try { apiKey = decrypt(embRow.api_key_encrypted as string, config.encryptionKey); } catch { - // Corrupted key / encryption-key mismatch — semantic search cannot proceed - return []; + // Corrupted key / encryption-key mismatch — a genuine config failure that + // must be surfaced rather than reported as "no matches". + throw new Error('Semantic search unavailable: embedding API key could not be decrypted'); } } const baseUrl = (embRow.base_url as string) ?? ''; @@ -190,7 +196,11 @@ export class SqliteMcpProvider implements McpDataProvider { const apiType = (embRow.api_type as string) ?? 'openai'; const vector = await this.embedQuery(apiType, baseUrl, apiKey, model, query); - if (!vector) return []; + if (!vector) { + // Embedding provider auth/timeout/outage — surface instead of faking a + // zero-result search. + throw new Error('Semantic search unavailable: embedding request failed'); + } try { const res = await axios.post( @@ -205,8 +215,11 @@ export class SqliteMcpProvider implements McpDataProvider { score: typeof m.score === 'number' ? m.score : 0, })) .filter((m) => !!m.fullName); - } catch { - return []; + } catch (err) { + // Vector-worker outage/auth/timeout — surface so the client can distinguish + // an outage from a valid zero-result search. + logger.errorFromError('mcp.semantic', 'Vector worker query failed', err as Error); + throw new Error('Semantic search unavailable: vector search worker request failed'); } } @@ -246,7 +259,8 @@ export class SqliteMcpProvider implements McpDataProvider { ); if (apiType === 'cohere') return (res.data?.embeddings?.[0] as number[]) ?? null; return (res.data?.data?.[0]?.embedding as number[]) ?? null; - } catch { + } catch (err) { + logger.errorFromError('mcp.embed', 'Embedding request failed', err as Error); return null; } } diff --git a/server/src/mcp/tools.ts b/server/src/mcp/tools.ts index 648744d0..7ec3c953 100644 --- a/server/src/mcp/tools.ts +++ b/server/src/mcp/tools.ts @@ -24,7 +24,8 @@ export function registerMcpTools(server: McpServer, provider: McpDataProvider, v isAnalyzed: z.boolean().optional().describe('Only include AI-analyzed repositories when true'), sortBy: z.enum(['stars', 'updated', 'name', 'starred']).optional(), sortOrder: z.enum(['desc', 'asc']).optional(), - limit: z.number().optional().describe('Max results (1-200, default 50)'), + limit: z.number().int().min(1).max(200).optional().describe('Max results (1-200, default 50)'), + offset: z.number().int().min(0).optional().describe('Results to skip for pagination'), }, }, async (args) => { @@ -39,6 +40,7 @@ export function registerMcpTools(server: McpServer, provider: McpDataProvider, v sortBy: args.sortBy, sortOrder: args.sortOrder, limit: args.limit, + offset: args.offset, }); return json(repos); } @@ -101,7 +103,7 @@ export function registerMcpTools(server: McpServer, provider: McpDataProvider, v 'List recent releases of starred repositories, optionally filtered by publish date (ISO string, inclusive).', inputSchema: { since: z.string().optional().describe('Only releases published at or after this ISO datetime'), - limit: z.number().optional().describe('Max results (1-200, default 50)'), + limit: z.number().int().min(1).max(200).optional().describe('Max results (1-200, default 50)'), }, }, async (args) => { @@ -129,7 +131,7 @@ export function registerMcpTools(server: McpServer, provider: McpDataProvider, v 'Semantic / natural-language search over starred repositories using the configured vector index. Returns repositories ranked by embedding similarity to the query.', inputSchema: { query: z.string().describe('Natural-language query, e.g. "a lightweight Rust CLI for PDF merging"'), - topK: z.number().optional().describe('Number of results (default 10)'), + topK: z.number().int().min(1).max(50).optional().describe('Number of results (1-50, default 10)'), }, }, async (args) => { diff --git a/server/src/routes/configs.ts b/server/src/routes/configs.ts index 7a151b1d..32af1c5e 100644 --- a/server/src/routes/configs.ts +++ b/server/src/routes/configs.ts @@ -762,7 +762,7 @@ router.get('/api/configs/mcp', (req, res) => { const row = db.prepare('SELECT * FROM mcp_configs WHERE id = ?').get('default') as Record | undefined; if (!row) { - res.json({ enabled: false, port: 0, token: '', tokenStatus: 'empty' }); + res.json({ enabled: false, port: 18789, token: '', tokenStatus: 'empty' }); return; } @@ -780,7 +780,7 @@ router.get('/api/configs/mcp', (req, res) => { res.json({ enabled: !!row.enabled, - port: typeof row.port === 'number' ? row.port : 0, + port: typeof row.port === 'number' ? row.port : 18789, token, tokenStatus, }); @@ -809,6 +809,14 @@ router.put('/api/configs/mcp', (req, res) => { const isEnabled = enabled === true || enabled === 1; + if (isEnabled && !encryptedToken) { + res.status(400).json({ + error: 'MCP token is required when enabled', + code: 'MCP_TOKEN_REQUIRED', + }); + return; + } + if (typeof port !== 'number' || !Number.isInteger(port) || port < 1 || port > 65535) { res.status(400).json({ error: 'Invalid port: must be an integer between 1 and 65535', diff --git a/src/services/backendAdapter.ts b/src/services/backendAdapter.ts index 8e964322..ae9acb03 100644 --- a/src/services/backendAdapter.ts +++ b/src/services/backendAdapter.ts @@ -79,7 +79,7 @@ class BackendAdapter { } return headers; } - private async fetchWithTimeout(url: string, options?: RequestInit, timeoutMs = 30000): Promise { + private async fetchWithTimeout(url: string, options?: RequestInit, timeoutMs = 30000, sensitive = false): Promise { const startTime = Date.now(); const method = (options?.method || 'GET').toUpperCase(); const path = url.replace(/^https?:\/\/[^/]+/, ''); @@ -143,6 +143,23 @@ class BackendAdapter { const text = await cloned.text(); if (text.length > 0) { responseBody = text.length > 4000 ? text.slice(0, 4000) + '...[truncated]' : text; + if (sensitive) { + // Redact secret fields (e.g. decrypted MCP token) before logging. + try { + const parsed = JSON.parse(text) as Record; + responseBody = JSON.stringify( + Object.fromEntries( + Object.entries(parsed).map(([k, v]) => [ + k, + /token|secret|api[_-]?key|password/i.test(k) ? '***' : v, + ]) + ), + 2 + ); + } catch { + responseBody = '[redacted]'; + } + } } } catch { /* body not readable */ } logger.debug('backendAdapter', 'Backend request', { @@ -168,14 +185,14 @@ class BackendAdapter { * Retry wrapper with exponential backoff for transient network errors. * Covers browser fetch (Chrome/Firefox/Safari) and Node.js undici fetch. */ - private async fetchWithRetry(url: string, options?: RequestInit, timeoutMs = 30000, maxRetries = 3): Promise { + private async fetchWithRetry(url: string, options?: RequestInit, timeoutMs = 30000, maxRetries = 3, sensitive = false): Promise { const retryStartTime = Date.now(); const method = (options?.method || 'GET').toUpperCase(); const path = url.replace(/^https?:\/\/[^/]+/, ''); let lastError: Error | undefined; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { - return await this.fetchWithTimeout(url, options, timeoutMs); + return await this.fetchWithTimeout(url, options, timeoutMs, sensitive); } catch (err) { lastError = err as Error; const isRetryable = @@ -668,7 +685,7 @@ class BackendAdapter { const res = await this.fetchWithTimeout(`${this._backendUrl}/configs/mcp?decrypt=true`, { headers: this.getAuthHeaders() - }); + }, 30000, true); if (!res.ok) await this.throwTranslatedError(res, 'Fetch MCP config error'); const data = await res.json() as Record; return { diff --git a/src/services/indexedDbStorage.ts b/src/services/indexedDbStorage.ts index 0d0396f0..5349cbc7 100644 --- a/src/services/indexedDbStorage.ts +++ b/src/services/indexedDbStorage.ts @@ -271,7 +271,9 @@ export const indexedDBStorage: StateStorage = { } } else if (selected === lsRaw && selected !== idbRaw) { try { - await withTimeout(idbSet(name, stamp(selected))); + // Repair verbatim: preserve the selected snapshot's original + // timestamp so it never appears newer than a concurrent genuine write. + await withTimeout(idbSet(name, selected)); } catch (error) { console.warn('[storage] IndexedDB repair write failed', error); } @@ -289,8 +291,6 @@ export const indexedDBStorage: StateStorage = { if (typeof window === 'undefined') return; const stamped = stamp(normalizeValue(value)); - latestValue = { name, value: stamped }; - registerFlushListeners(); // Defensive gate: never let an empty/invalid snapshot clobber a // non-empty one. A failed hydration (migrate/merge error or an @@ -317,6 +317,13 @@ export const indexedDBStorage: StateStorage = { } } + // Only cache the value for unload-time flush *after* the empty-write gate + // has accepted it. Caching a rejected write would let pagehide/visibilitychange + // persist the empty snapshot to localStorage, bypassing the guard and + // destroying the only good copy in localStorage-only environments. + latestValue = { name, value: stamped }; + registerFlushListeners(); + // Primary path: IndexedDB first (large data friendly) if (canUseIndexedDB()) { try { diff --git a/src/services/mcpRendererBridge.ts b/src/services/mcpRendererBridge.ts index fa3b750b..5f0e61a7 100644 --- a/src/services/mcpRendererBridge.ts +++ b/src/services/mcpRendererBridge.ts @@ -134,7 +134,10 @@ export function initMcpRendererBridge(): void { })) .filter((m) => !!m.fullName); } catch { - return []; + // Surface delegated failures (auth/embedding/vector-worker outage) so + // Electron MCP clients can distinguish an outage from a valid zero-result + // search, rather than silently reporting "no matches". + throw new Error('Semantic search failed'); } }; @@ -153,13 +156,29 @@ export function initMcpRendererBridge(): void { } useAppStore.subscribe((state, prev) => { - if (state.mcpConfig.enabled && (!prev.mcpConfig.enabled || state.mcpConfig.token !== prev.mcpConfig.token)) { - // 刚启用或令牌变更,立即推送一次 - window.electronAPI?.startMcp?.({ port: state.mcpConfig.port || 18789, token: state.mcpConfig.token }); - window.electronAPI?.pushMcpSnapshot?.(buildSnapshot()); + // Disabled: stop the embedded server if it was running, so it doesn't + // linger with a stale snapshot. + if (!state.mcpConfig.enabled) { + if (prev.mcpConfig.enabled) { + window.electronAPI?.stopMcp?.(); + } return; } - if (!state.mcpConfig.enabled) return; + + // Enabled: (re)start when enabling, the token changes, or the port + // changes — the latter previously never rebound the running server. + if ( + !prev.mcpConfig.enabled || + state.mcpConfig.token !== prev.mcpConfig.token || + state.mcpConfig.port !== prev.mcpConfig.port + ) { + if (state.mcpConfig.token) { + window.electronAPI?.startMcp?.({ port: state.mcpConfig.port || 18789, token: state.mcpConfig.token }); + window.electronAPI?.pushMcpSnapshot?.(buildSnapshot()); + } + return; + } + // Only schedule a (debounced) snapshot push when data the snapshot actually // depends on changed. Plain UI state changes (e.g. selection, loading flags) // reuse the same references, so we skip the expensive rebuild. diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts index bfa060cd..8274d390 100644 --- a/src/store/useAppStore.ts +++ b/src/store/useAppStore.ts @@ -2233,8 +2233,9 @@ export const useAppStore = create()( // 持久化向量搜索状态(vectorCount 等,跨重启保留) vectorSearchStatus: state.vectorSearchStatus, - // 持久化 MCP 服务配置 - mcpConfig: state.mcpConfig, + // 持久化 MCP 服务配置(仅 enabled/port;token 不存入快照, + // 运行时从加密的后端存储按需取回,避免明文令牌落入 IndexedDB/localStorage 与备份) + mcpConfig: { enabled: state.mcpConfig.enabled, port: state.mcpConfig.port }, // 持久化WebDAV配置 webdavConfigs: state.webdavConfigs,