From bfef867ba7a5e785bab101d51a23e98698ee749c Mon Sep 17 00:00:00 2001 From: Adam Weidman <65992621+adamfweidman@users.noreply.github.com> Date: Tue, 26 Aug 2025 20:49:25 +0000 Subject: [PATCH] Add a2a-server package to gemini-cli (#6597) --- eslint.config.js | 6 +- package-lock.json | 2415 +++++++++++++++-- package.json | 1 + packages/a2a-server/README.md | 5 + packages/a2a-server/index.ts | 7 + packages/a2a-server/package.json | 48 + packages/a2a-server/src/agent.test.ts | 648 +++++ packages/a2a-server/src/agent.ts | 785 ++++++ packages/a2a-server/src/config.ts | 203 ++ packages/a2a-server/src/endpoints.test.ts | 146 + packages/a2a-server/src/extension.ts | 118 + packages/a2a-server/src/gcs.test.ts | 340 +++ packages/a2a-server/src/gcs.ts | 308 +++ packages/a2a-server/src/index.ts | 8 + packages/a2a-server/src/logger.ts | 28 + packages/a2a-server/src/metadata_types.ts | 33 + packages/a2a-server/src/server.ts | 33 + packages/a2a-server/src/settings.ts | 154 ++ packages/a2a-server/src/task.ts | 930 +++++++ packages/a2a-server/src/testing_utils.ts | 180 ++ packages/a2a-server/src/types.ts | 104 + packages/a2a-server/tsconfig.json | 11 + packages/a2a-server/vitest.config.ts | 26 + packages/cli/package.json | 4 +- .../ui/hooks/slashCommandProcessor.test.ts | 1 + packages/core/package.json | 1 + packages/vscode-ide-companion/tsconfig.json | 7 + 27 files changed, 6246 insertions(+), 304 deletions(-) create mode 100644 packages/a2a-server/README.md create mode 100644 packages/a2a-server/index.ts create mode 100644 packages/a2a-server/package.json create mode 100644 packages/a2a-server/src/agent.test.ts create mode 100644 packages/a2a-server/src/agent.ts create mode 100644 packages/a2a-server/src/config.ts create mode 100644 packages/a2a-server/src/endpoints.test.ts create mode 100644 packages/a2a-server/src/extension.ts create mode 100644 packages/a2a-server/src/gcs.test.ts create mode 100644 packages/a2a-server/src/gcs.ts create mode 100644 packages/a2a-server/src/index.ts create mode 100644 packages/a2a-server/src/logger.ts create mode 100644 packages/a2a-server/src/metadata_types.ts create mode 100644 packages/a2a-server/src/server.ts create mode 100644 packages/a2a-server/src/settings.ts create mode 100644 packages/a2a-server/src/task.ts create mode 100644 packages/a2a-server/src/testing_utils.ts create mode 100644 packages/a2a-server/src/types.ts create mode 100644 packages/a2a-server/tsconfig.json create mode 100644 packages/a2a-server/vitest.config.ts diff --git a/eslint.config.js b/eslint.config.js index cb601fe797..50efb79cde 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -31,11 +31,7 @@ export default tseslint.config( 'node_modules/*', '.integration-tests/**', 'eslint.config.js', - 'packages/cli/dist/**', - 'packages/core/dist/**', - 'packages/server/dist/**', - 'packages/test-utils/dist/**', - 'packages/vscode-ide-companion/dist/**', + 'packages/**/dist/**', 'bundle/**', 'package/bundle/**', '.integration-tests/**', diff --git a/package-lock.json b/package-lock.json index 9e025c1466..0cef0ecf08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,6 +63,38 @@ "node-pty": "^1.0.0" } }, + "node_modules/@a2a-js/sdk": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@a2a-js/sdk/-/sdk-0.3.2.tgz", + "integrity": "sha512-maqxdZ/xeuSRywObfBTvwXbXvkDMmKVkiY8K9rCHDwm0QYUJuu512GnNrwuxkKTwXpNyByzEPg3RYfBveRl96w==", + "dependencies": { + "uuid": "^11.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "express": "^4.21.2" + }, + "peerDependenciesMeta": { + "express": { + "optional": true + } + } + }, + "node_modules/@a2a-js/sdk/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/@alcalzone/ansi-tokenize": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.0.tgz", @@ -267,6 +299,15 @@ "node": ">=6" } }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", @@ -382,6 +423,17 @@ "node": ">=18" } }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.6", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", @@ -991,10 +1043,80 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@google-cloud/paginator": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", + "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", + "license": "Apache-2.0", + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.17.0.tgz", + "integrity": "sha512-5m9GoZqKh52a1UqkxDBu/+WVFDALNtHg5up5gNmNbXQWBcV813tzJKsyDtKjOPrlR1em1TxtD7NSPCrObH7koQ==", + "license": "Apache-2.0", + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "<4.1.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^4.4.1", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@google/gemini-cli": { "resolved": "packages/cli", "link": true }, + "node_modules/@google/gemini-cli-a2a-server": { + "resolved": "packages/a2a-server", + "link": true + }, "node_modules/@google/gemini-cli-core": { "resolved": "packages/core", "link": true @@ -1311,6 +1433,18 @@ "node": ">=12" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -1617,6 +1751,224 @@ "node": ">=18" } }, + "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.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@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/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@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/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/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.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@mswjs/interceptors": { "version": "0.39.5", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.5.tgz", @@ -1635,6 +1987,19 @@ "node": ">=18" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2209,6 +2574,16 @@ "node": ">=14" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2673,6 +3048,15 @@ "node": ">=14.16" } }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -2692,6 +3076,12 @@ "@types/node": "*" } }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "license": "MIT" + }, "node_modules/@types/chai": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", @@ -2732,6 +3122,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/cors": { "version": "2.8.19", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", @@ -2798,6 +3195,17 @@ "@types/send": "*" } }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, "node_modules/@types/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", @@ -2859,6 +3267,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/marked": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz", @@ -2866,6 +3284,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -2952,6 +3377,56 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/request": { + "version": "2.48.13", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", + "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", + "license": "MIT", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.5" + } + }, + "node_modules/@types/request/node_modules/form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "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.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/@types/request/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@types/request/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@types/semver": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", @@ -2996,6 +3471,51 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/tar": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.13.tgz", + "integrity": "sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "minipass": "^4.0.0" + } + }, + "node_modules/@types/tar/node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, "node_modules/@types/tinycolor2": { "version": "1.4.6", "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", @@ -3006,7 +3526,12 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", - "dev": true, + "license": "MIT" + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, "node_modules/@types/unist": { @@ -3510,14 +4035,50 @@ "integrity": "sha512-5xXB7kdQlFBP82ViMJTwwEc3gKCLGKR/eoxQm4zge7GPBl86tCdI0IdPJjoKd8mUSFXz5V7i/25sfsEkP4j46g==", "license": "MIT" }, - "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==", + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "license": "MIT", "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "1.52.0" }, "engines": { "node": ">= 0.6" @@ -3734,6 +4295,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT", + "peer": true + }, "node_modules/array-includes": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", @@ -3877,6 +4445,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -3899,6 +4483,12 @@ "js-tokens": "^9.0.1" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -3909,6 +4499,21 @@ "node": ">= 0.4" } }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "license": "MIT", + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/atomically": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.0.3.tgz", @@ -3982,23 +4587,74 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "license": "MIT", + "peer": true, "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" }, "engines": { - "node": ">=18" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", + "peer": true + }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, "node_modules/boxen": { @@ -4266,6 +4922,15 @@ "node": ">= 16" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/cjs-module-lexer": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", @@ -4484,6 +5149,16 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4502,12 +5177,69 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/command-exists": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", "license": "MIT" }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4576,10 +5308,11 @@ } }, "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "5.2.1" }, @@ -4615,13 +5348,18 @@ } }, "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT", - "engines": { - "node": ">=6.6.0" - } + "peer": true + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" }, "node_modules/cors": { "version": "2.8.5", @@ -4956,6 +5694,15 @@ "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==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -4974,6 +5721,17 @@ "node": ">=6" } }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -4987,6 +5745,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/diff": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", @@ -5125,6 +5894,18 @@ "node": ">= 0.4" } }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -5152,6 +5933,12 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -5342,7 +6129,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5835,6 +6621,15 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -5905,41 +6700,46 @@ } }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", + "peer": true, "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" }, "engines": { - "node": ">= 18" + "node": ">= 0.10.0" }, "funding": { "type": "opencollective", @@ -5961,6 +6761,43 @@ "express": ">= 4.11" } }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", + "peer": true + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -6051,6 +6888,13 @@ "dev": true, "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", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", @@ -6067,6 +6911,24 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-xml-parser": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", + "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.1.1" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -6086,6 +6948,12 @@ "pend": "~1.2.0" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, "node_modules/figures": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", @@ -6128,22 +6996,51 @@ } }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "license": "MIT", + "peer": true, "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" + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", + "peer": true + }, + "node_modules/finalhandler/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6194,6 +7091,12 @@ "dev": true, "license": "ISC" }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -6226,6 +7129,23 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "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/form-data-encoder": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz", @@ -6235,6 +7155,47 @@ "node": ">= 18" } }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -6251,12 +7212,13 @@ "license": "MIT" }, "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "license": "MIT", + "peer": true, "engines": { - "node": ">= 0.8" + "node": ">= 0.6" } }, "node_modules/fs-extra": { @@ -6343,21 +7305,7 @@ "integrity": "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==", "license": "BSD-3-Clause" }, - "node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/gcp-metadata/node_modules/gaxios": { + "node_modules/gaxios": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", @@ -6373,46 +7321,18 @@ "node": ">=14" } }, - "node_modules/gcp-metadata/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", "dependencies": { - "whatwg-url": "^5.0.0" + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/gcp-metadata/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/gcp-metadata/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/gcp-metadata/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "node": ">=14" } }, "node_modules/gemini-cli-vscode-ide-companion": { @@ -6655,64 +7575,6 @@ "node": ">=14" } }, - "node_modules/google-auth-library/node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/google-auth-library/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/google-auth-library/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/google-auth-library/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/google-auth-library/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/google-logging-utils": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", @@ -6820,64 +7682,6 @@ "node": ">=14.0.0" } }, - "node_modules/gtoken/node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/gtoken/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/gtoken/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/gtoken/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/gtoken/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -6945,7 +7749,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -7010,6 +7813,22 @@ "node": ">=18" } }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -8346,6 +9165,12 @@ "json-buffer": "3.0.1" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, "node_modules/ky": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/ky/-/ky-1.8.1.tgz", @@ -8462,6 +9287,23 @@ "dev": true, "license": "MIT" }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -8610,12 +9452,13 @@ } }, "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "license": "MIT", + "peer": true, "engines": { - "node": ">= 0.8" + "node": ">= 0.6" } }, "node_modules/memfs": { @@ -8648,13 +9491,11 @@ } }, "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==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "license": "MIT", - "engines": { - "node": ">=18" - }, + "peer": true, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -8669,6 +9510,15 @@ "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -8683,6 +9533,18 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -8756,6 +9618,33 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/mnemonist": { "version": "0.40.3", "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.3.tgz", @@ -8896,10 +9785,11 @@ "license": "MIT" }, "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -8911,6 +9801,48 @@ "dev": true, "license": "MIT" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-pty": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", @@ -9367,6 +10299,15 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -9456,7 +10397,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -9673,13 +10613,11 @@ } }, "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT", - "engines": { - "node": ">=16" - } + "peer": true }, "node_modules/path-type": { "version": "3.0.0", @@ -9994,12 +10932,12 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -10238,6 +11176,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -10441,6 +11393,29 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "license": "MIT", + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -10508,6 +11483,15 @@ "node": ">= 18" } }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/rrweb-cssom": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", @@ -10636,6 +11620,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -10684,40 +11677,94 @@ } }, "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "license": "MIT", + "peer": true, "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" }, "engines": { - "node": ">= 18" + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", + "peer": true + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "peer": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" } }, "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "license": "MIT", + "peer": true, "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" }, "engines": { - "node": ">= 18" + "node": ">= 0.8.0" } }, "node_modules/set-function-length": { @@ -10914,6 +11961,21 @@ "url": "https://github.com/steveukx/git-js?sponsor=1" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/slice-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", @@ -10999,6 +12061,15 @@ "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", "license": "CC0-1.0" }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -11057,6 +12128,21 @@ "node": ">= 0.4" } }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, "node_modules/strict-event-emitter": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", @@ -11064,6 +12150,15 @@ "dev": true, "license": "MIT" }, + "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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -11324,11 +12419,77 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/stubborn-fs": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-1.2.5.tgz", "integrity": "sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==" }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT" + }, + "node_modules/superagent": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", + "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.4", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/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/supertest": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", + "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^10.2.3" + }, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -11364,6 +12525,78 @@ "dev": true, "license": "MIT" }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "license": "Apache-2.0", + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/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==", + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/temp-dir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", @@ -11444,6 +12677,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, "node_modules/thingies": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", @@ -11657,6 +12896,15 @@ "tree-kill": "cli.js" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -11736,14 +12984,37 @@ } }, "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "license": "MIT", + "peer": true, "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "1.52.0" }, "engines": { "node": ">= 0.6" @@ -12125,6 +13396,22 @@ "requires-port": "^1.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -12571,6 +13858,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -12734,6 +14057,15 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -12816,7 +14148,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -12874,6 +14205,277 @@ "zod": "^3.24.1" } }, + "packages/a2a-server": { + "name": "@google/gemini-cli-a2a-server", + "version": "0.1.0", + "dependencies": { + "@a2a-js/sdk": "^0.3.2", + "@google-cloud/storage": "^7.16.0", + "@google/gemini-cli-core": "file:../core", + "express": "^5.1.0", + "fs-extra": "^11.3.0", + "tar": "^7.4.3", + "uuid": "^11.1.0", + "winston": "^3.17.0" + }, + "devDependencies": { + "@types/express": "^5.0.3", + "@types/fs-extra": "^11.0.4", + "@types/supertest": "^6.0.3", + "@types/tar": "^6.1.13", + "dotenv": "^16.4.5", + "supertest": "^7.1.4", + "typescript": "^5.3.3", + "vitest": "^3.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "packages/a2a-server/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" + } + }, + "packages/a2a-server/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "packages/a2a-server/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "packages/a2a-server/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" + } + }, + "packages/a2a-server/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "packages/a2a-server/node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/a2a-server/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "packages/a2a-server/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" + } + }, + "packages/a2a-server/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" + } + }, + "packages/a2a-server/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" + } + }, + "packages/a2a-server/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" + } + }, + "packages/a2a-server/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "packages/a2a-server/node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "packages/a2a-server/node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "packages/a2a-server/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "packages/a2a-server/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "packages/cli": { "name": "@google/gemini-cli", "version": "0.1.21", @@ -13077,6 +14679,7 @@ "chardet": "^2.1.0", "diff": "^7.0.0", "dotenv": "^17.1.0", + "fast-uri": "^3.0.6", "fdir": "^6.4.6", "fzf": "^0.5.2", "glob": "^10.4.5", @@ -13216,6 +14819,224 @@ "integrity": "sha512-30sjmas1hQ0gVbX68LAWlm/YYlEqUErunPJJKLpEl+xhK0mKn+jyzlCOpsdTwfkZfPy4U6CDkmygBLC3AB8W9Q==", "dev": true, "license": "MIT" + }, + "packages/vscode-ide-companion/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" + } + }, + "packages/vscode-ide-companion/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "packages/vscode-ide-companion/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "packages/vscode-ide-companion/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" + } + }, + "packages/vscode-ide-companion/node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "packages/vscode-ide-companion/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "packages/vscode-ide-companion/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" + } + }, + "packages/vscode-ide-companion/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" + } + }, + "packages/vscode-ide-companion/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" + } + }, + "packages/vscode-ide-companion/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" + } + }, + "packages/vscode-ide-companion/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "packages/vscode-ide-companion/node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "packages/vscode-ide-companion/node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "packages/vscode-ide-companion/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } } } } diff --git a/package.json b/package.json index 1036818d59..38c3f527bc 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ }, "scripts": { "start": "node scripts/start.js", + "start:a2a-server": "CODER_AGENT_PORT=41242 npm run start --workspace @google/gemini-cli-a2a-server", "debug": "cross-env DEBUG=1 node --inspect-brk scripts/start.js", "auth:npm": "npx google-artifactregistry-auth", "auth:docker": "gcloud auth configure-docker us-west1-docker.pkg.dev", diff --git a/packages/a2a-server/README.md b/packages/a2a-server/README.md new file mode 100644 index 0000000000..bd6a2fac45 --- /dev/null +++ b/packages/a2a-server/README.md @@ -0,0 +1,5 @@ +# Gemini CLI A2A Server + +## All code in this package is experimental and under active development + +This package contains the A2A server implementation for the Gemini CLI. diff --git a/packages/a2a-server/index.ts b/packages/a2a-server/index.ts new file mode 100644 index 0000000000..3e74d6beda --- /dev/null +++ b/packages/a2a-server/index.ts @@ -0,0 +1,7 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './src/index.js'; diff --git a/packages/a2a-server/package.json b/packages/a2a-server/package.json new file mode 100644 index 0000000000..edbd64f17d --- /dev/null +++ b/packages/a2a-server/package.json @@ -0,0 +1,48 @@ +{ + "name": "@google/gemini-cli-a2a-server", + "version": "0.1.0", + "private": true, + "description": "Gemini CLI A2A Server", + "repository": { + "type": "git", + "url": "git+https://github.com/google-gemini/gemini-cli.git", + "directory": "packages/a2a-server" + }, + "type": "module", + "main": "dist/server.js", + "scripts": { + "start": "node dist/src/server.js", + "build": "node ../../scripts/build_package.js", + "lint": "eslint . --ext .ts,.tsx", + "format": "prettier --write .", + "test": "vitest run", + "test:ci": "vitest run --coverage", + "typecheck": "tsc --noEmit" + }, + "files": [ + "dist" + ], + "dependencies": { + "@a2a-js/sdk": "^0.3.2", + "@google-cloud/storage": "^7.16.0", + "@google/gemini-cli-core": "file:../core", + "express": "^5.1.0", + "fs-extra": "^11.3.0", + "tar": "^7.4.3", + "uuid": "^11.1.0", + "winston": "^3.17.0" + }, + "devDependencies": { + "@types/express": "^5.0.3", + "@types/fs-extra": "^11.0.4", + "@types/supertest": "^6.0.3", + "@types/tar": "^6.1.13", + "dotenv": "^16.4.5", + "supertest": "^7.1.4", + "typescript": "^5.3.3", + "vitest": "^3.1.1" + }, + "engines": { + "node": ">=20" + } +} diff --git a/packages/a2a-server/src/agent.test.ts b/packages/a2a-server/src/agent.test.ts new file mode 100644 index 0000000000..04160d3f23 --- /dev/null +++ b/packages/a2a-server/src/agent.test.ts @@ -0,0 +1,648 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { Config } from '@google/gemini-cli-core'; +import { + GeminiEventType, + ApprovalMode, + type ToolCallConfirmationDetails, +} from '@google/gemini-cli-core'; +import type { + TaskStatusUpdateEvent, + SendStreamingMessageSuccessResponse, +} from '@a2a-js/sdk'; +import type express from 'express'; +import type { Server } from 'node:http'; +import request from 'supertest'; +import { + afterAll, + afterEach, + beforeEach, + beforeAll, + describe, + expect, + it, + vi, +} from 'vitest'; +import { createApp } from './agent.js'; +import { + assertUniqueFinalEventIsLast, + assertTaskCreationAndWorkingStatus, + createStreamMessageRequest, + MockTool, +} from './testing_utils.js'; + +const mockToolConfirmationFn = async () => + ({}) as unknown as ToolCallConfirmationDetails; + +const streamToSSEEvents = ( + stream: string, +): SendStreamingMessageSuccessResponse[] => + stream + .split('\n\n') + .filter(Boolean) // Remove empty strings from trailing newlines + .map((chunk) => { + const dataLine = chunk + .split('\n') + .find((line) => line.startsWith('data: ')); + if (!dataLine) { + throw new Error(`Invalid SSE chunk found: "${chunk}"`); + } + return JSON.parse(dataLine.substring(6)); + }); + +// Mock the logger to avoid polluting test output +// Comment out to debug tests +vi.mock('./logger.js', () => ({ + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, +})); + +let config: Config; +const getToolRegistrySpy = vi.fn().mockReturnValue(ApprovalMode.DEFAULT); +const getApprovalModeSpy = vi.fn(); +vi.mock('./config.js', async () => { + const actual = await vi.importActual('./config.js'); + return { + ...actual, + loadConfig: vi.fn().mockImplementation(async () => { + config = { + getToolRegistry: getToolRegistrySpy, + getApprovalMode: getApprovalModeSpy, + getIdeMode: vi.fn().mockReturnValue(false), + getAllowedTools: vi.fn().mockReturnValue([]), + getIdeClient: vi.fn(), + getWorkspaceContext: vi.fn().mockReturnValue({ + isPathWithinWorkspace: () => true, + }), + getTargetDir: () => '/test', + getGeminiClient: vi.fn(), + getDebugMode: vi.fn().mockReturnValue(false), + getContentGeneratorConfig: vi + .fn() + .mockReturnValue({ model: 'gemini-pro' }), + getModel: vi.fn().mockReturnValue('gemini-pro'), + getUsageStatisticsEnabled: vi.fn().mockReturnValue(false), + setFlashFallbackHandler: vi.fn(), + initialize: vi.fn().mockResolvedValue(undefined), + } as unknown as Config; + return config; + }), + }; +}); + +// Mock the GeminiClient to avoid actual API calls +const sendMessageStreamSpy = vi.fn(); +vi.mock('@google/gemini-cli-core', async () => { + const actual = await vi.importActual('@google/gemini-cli-core'); + return { + ...actual, + GeminiClient: vi.fn().mockImplementation(() => ({ + sendMessageStream: sendMessageStreamSpy, + getUserTier: vi.fn().mockReturnValue('free'), + initialize: vi.fn(), + })), + }; +}); + +describe('E2E Tests', () => { + let app: express.Express; + let server: Server; + + beforeAll(async () => { + app = await createApp(); + server = app.listen(0); // Listen on a random available port + }); + + beforeEach(() => { + getApprovalModeSpy.mockReturnValue(ApprovalMode.DEFAULT); + }); + + afterAll( + () => + new Promise((resolve) => { + server.close(() => { + resolve(); + }); + }), + ); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should create a new task and stream status updates (text-content) via POST /', async () => { + sendMessageStreamSpy.mockImplementation(async function* () { + yield* [{ type: 'content', value: 'Hello how are you?' }]; + }); + + const agent = request.agent(app); + const res = await agent + .post('/') + .send(createStreamMessageRequest('hello', 'a2a-test-message')) + .set('Content-Type', 'application/json') + .expect(200); + + const events = streamToSSEEvents(res.text); + + assertTaskCreationAndWorkingStatus(events); + + // Status update: text-content + const textContentEvent = events[2].result as TaskStatusUpdateEvent; + expect(textContentEvent.kind).toBe('status-update'); + expect(textContentEvent.status.state).toBe('working'); + expect(textContentEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'text-content', + }); + expect(textContentEvent.status.message?.parts).toMatchObject([ + { kind: 'text', text: 'Hello how are you?' }, + ]); + + // Status update: input-required (final) + const finalEvent = events[3].result as TaskStatusUpdateEvent; + expect(finalEvent.kind).toBe('status-update'); + expect(finalEvent.status?.state).toBe('input-required'); + expect(finalEvent.final).toBe(true); + + assertUniqueFinalEventIsLast(events); + expect(events.length).toBe(4); + }); + + it('should create a new task, schedule a tool call, and wait for approval', async () => { + // First call yields the tool request + sendMessageStreamSpy.mockImplementationOnce(async function* () { + yield* [ + { + type: GeminiEventType.ToolCallRequest, + value: { + callId: 'test-call-id', + name: 'test-tool', + args: {}, + }, + }, + ]; + }); + // Subsequent calls yield nothing + sendMessageStreamSpy.mockImplementation(async function* () { + yield* []; + }); + + const mockTool = new MockTool( + 'test-tool', + 'Test Tool', + true, + false, + mockToolConfirmationFn, + ); + + getToolRegistrySpy.mockReturnValue({ + getAllTools: vi.fn().mockReturnValue([mockTool]), + getToolsByServer: vi.fn().mockReturnValue([]), + getTool: vi.fn().mockReturnValue(mockTool), + }); + + const agent = request.agent(app); + const res = await agent + .post('/') + .send(createStreamMessageRequest('run a tool', 'a2a-tool-test-message')) + .set('Content-Type', 'application/json') + .expect(200); + + const events = streamToSSEEvents(res.text); + assertTaskCreationAndWorkingStatus(events); + + // Status update: working + const workingEvent2 = events[2].result as TaskStatusUpdateEvent; + expect(workingEvent2.kind).toBe('status-update'); + expect(workingEvent2.status.state).toBe('working'); + expect(workingEvent2.metadata?.['coderAgent']).toMatchObject({ + kind: 'state-change', + }); + + // Status update: tool-call-update + const toolCallUpdateEvent = events[3].result as TaskStatusUpdateEvent; + expect(toolCallUpdateEvent.kind).toBe('status-update'); + expect(toolCallUpdateEvent.status.state).toBe('working'); + expect(toolCallUpdateEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(toolCallUpdateEvent.status.message?.parts).toMatchObject([ + { + data: { + status: 'validating', + request: { callId: 'test-call-id' }, + }, + }, + ]); + + // State update: awaiting_approval update + const toolCallConfirmationEvent = events[4].result as TaskStatusUpdateEvent; + expect(toolCallConfirmationEvent.kind).toBe('status-update'); + expect(toolCallConfirmationEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-confirmation', + }); + expect(toolCallConfirmationEvent.status.message?.parts).toMatchObject([ + { + data: { + status: 'awaiting_approval', + request: { callId: 'test-call-id' }, + }, + }, + ]); + expect(toolCallConfirmationEvent.status?.state).toBe('working'); + + assertUniqueFinalEventIsLast(events); + expect(events.length).toBe(6); + }); + + it('should handle multiple tool calls in a single turn', async () => { + // First call yields the tool request + sendMessageStreamSpy.mockImplementationOnce(async function* () { + yield* [ + { + type: GeminiEventType.ToolCallRequest, + value: { + callId: 'test-call-id-1', + name: 'test-tool-1', + args: {}, + }, + }, + { + type: GeminiEventType.ToolCallRequest, + value: { + callId: 'test-call-id-2', + name: 'test-tool-2', + args: {}, + }, + }, + ]; + }); + // Subsequent calls yield nothing + sendMessageStreamSpy.mockImplementation(async function* () { + yield* []; + }); + + const mockTool1 = new MockTool( + 'test-tool-1', + 'Test Tool 1', + false, + false, + mockToolConfirmationFn, + ); + const mockTool2 = new MockTool( + 'test-tool-2', + 'Test Tool 2', + false, + false, + mockToolConfirmationFn, + ); + + getToolRegistrySpy.mockReturnValue({ + getAllTools: vi.fn().mockReturnValue([mockTool1, mockTool2]), + getToolsByServer: vi.fn().mockReturnValue([]), + getTool: vi.fn().mockImplementation((name: string) => { + if (name === 'test-tool-1') return mockTool1; + if (name === 'test-tool-2') return mockTool2; + return undefined; + }), + }); + + const agent = request.agent(app); + const res = await agent + .post('/') + .send( + createStreamMessageRequest( + 'run two tools', + 'a2a-multi-tool-test-message', + ), + ) + .set('Content-Type', 'application/json') + .expect(200); + + const events = streamToSSEEvents(res.text); + assertTaskCreationAndWorkingStatus(events); + + // Second working update + const workingEvent = events[2].result as TaskStatusUpdateEvent; + expect(workingEvent.kind).toBe('status-update'); + expect(workingEvent.status.state).toBe('working'); + + // State Update: Validate each tool call + const toolCallValidateEvent1 = events[3].result as TaskStatusUpdateEvent; + expect(toolCallValidateEvent1.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(toolCallValidateEvent1.status.message?.parts).toMatchObject([ + { + data: { + status: 'validating', + request: { callId: 'test-call-id-1' }, + }, + }, + ]); + const toolCallValidateEvent2 = events[4].result as TaskStatusUpdateEvent; + expect(toolCallValidateEvent2.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(toolCallValidateEvent2.status.message?.parts).toMatchObject([ + { + data: { + status: 'validating', + request: { callId: 'test-call-id-2' }, + }, + }, + ]); + + // State Update: Set each tool call to awaiting + const toolCallAwaitEvent1 = events[5].result as TaskStatusUpdateEvent; + expect(toolCallAwaitEvent1.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-confirmation', + }); + expect(toolCallAwaitEvent1.status.message?.parts).toMatchObject([ + { + data: { + status: 'awaiting_approval', + request: { callId: 'test-call-id-1' }, + }, + }, + ]); + const toolCallAwaitEvent2 = events[6].result as TaskStatusUpdateEvent; + expect(toolCallAwaitEvent2.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-confirmation', + }); + expect(toolCallAwaitEvent2.status.message?.parts).toMatchObject([ + { + data: { + status: 'awaiting_approval', + request: { callId: 'test-call-id-2' }, + }, + }, + ]); + + assertUniqueFinalEventIsLast(events); + expect(events.length).toBe(8); + }); + + it('should handle tool calls that do not require approval', async () => { + // First call yields the tool request + sendMessageStreamSpy.mockImplementationOnce(async function* () { + yield* [ + { + type: GeminiEventType.ToolCallRequest, + value: { + callId: 'test-call-id-no-approval', + name: 'test-tool-no-approval', + args: {}, + }, + }, + ]; + }); + // Second call, after the tool runs, yields the final text + sendMessageStreamSpy.mockImplementationOnce(async function* () { + yield* [{ type: 'content', value: 'Tool executed successfully.' }]; + }); + + const mockTool = new MockTool( + 'test-tool-no-approval', + 'Test Tool No Approval', + ); + mockTool.execute.mockResolvedValue({ + llmContent: 'Tool executed successfully.', + returnDisplay: 'Tool executed successfully.', + }); + + getToolRegistrySpy.mockReturnValue({ + getAllTools: vi.fn().mockReturnValue([mockTool]), + getToolsByServer: vi.fn().mockReturnValue([]), + getTool: vi.fn().mockReturnValue(mockTool), + }); + + const agent = request.agent(app); + const res = await agent + .post('/') + .send( + createStreamMessageRequest( + 'run a tool without approval', + 'a2a-no-approval-test-message', + ), + ) + .set('Content-Type', 'application/json') + .expect(200); + + const events = streamToSSEEvents(res.text); + assertTaskCreationAndWorkingStatus(events); + + // Status update: working + const workingEvent2 = events[2].result as TaskStatusUpdateEvent; + expect(workingEvent2.kind).toBe('status-update'); + expect(workingEvent2.status.state).toBe('working'); + + // Status update: tool-call-update (validating) + const validatingEvent = events[3].result as TaskStatusUpdateEvent; + expect(validatingEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(validatingEvent.status.message?.parts).toMatchObject([ + { + data: { + status: 'validating', + request: { callId: 'test-call-id-no-approval' }, + }, + }, + ]); + + // Status update: tool-call-update (scheduled) + const scheduledEvent = events[4].result as TaskStatusUpdateEvent; + expect(scheduledEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(scheduledEvent.status.message?.parts).toMatchObject([ + { + data: { + status: 'scheduled', + request: { callId: 'test-call-id-no-approval' }, + }, + }, + ]); + + // Status update: tool-call-update (executing) + const executingEvent = events[5].result as TaskStatusUpdateEvent; + expect(executingEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(executingEvent.status.message?.parts).toMatchObject([ + { + data: { + status: 'executing', + request: { callId: 'test-call-id-no-approval' }, + }, + }, + ]); + + // Status update: tool-call-update (success) + const successEvent = events[6].result as TaskStatusUpdateEvent; + expect(successEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(successEvent.status.message?.parts).toMatchObject([ + { + data: { + status: 'success', + request: { callId: 'test-call-id-no-approval' }, + }, + }, + ]); + + // Status update: working (before sending tool result to LLM) + const workingEvent3 = events[7].result as TaskStatusUpdateEvent; + expect(workingEvent3.kind).toBe('status-update'); + expect(workingEvent3.status.state).toBe('working'); + + // Status update: text-content (final LLM response) + const textContentEvent = events[8].result as TaskStatusUpdateEvent; + expect(textContentEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'text-content', + }); + expect(textContentEvent.status.message?.parts).toMatchObject([ + { text: 'Tool executed successfully.' }, + ]); + + assertUniqueFinalEventIsLast(events); + expect(events.length).toBe(10); + }); + + it('should bypass tool approval in YOLO mode', async () => { + // First call yields the tool request + sendMessageStreamSpy.mockImplementationOnce(async function* () { + yield* [ + { + type: GeminiEventType.ToolCallRequest, + value: { + callId: 'test-call-id-yolo', + name: 'test-tool-yolo', + args: {}, + }, + }, + ]; + }); + // Second call, after the tool runs, yields the final text + sendMessageStreamSpy.mockImplementationOnce(async function* () { + yield* [{ type: 'content', value: 'Tool executed successfully.' }]; + }); + + // Set approval mode to yolo + getApprovalModeSpy.mockReturnValue(ApprovalMode.YOLO); + + const mockTool = new MockTool( + 'test-tool-yolo', + 'Test Tool YOLO', + false, + false, + ); + mockTool.execute.mockResolvedValue({ + llmContent: 'Tool executed successfully.', + returnDisplay: 'Tool executed successfully.', + }); + + getToolRegistrySpy.mockReturnValue({ + getAllTools: vi.fn().mockReturnValue([mockTool]), + getToolsByServer: vi.fn().mockReturnValue([]), + getTool: vi.fn().mockReturnValue(mockTool), + }); + + const agent = request.agent(app); + const res = await agent + .post('/') + .send( + createStreamMessageRequest( + 'run a tool in yolo mode', + 'a2a-yolo-mode-test-message', + ), + ) + .set('Content-Type', 'application/json') + .expect(200); + + const events = streamToSSEEvents(res.text); + assertTaskCreationAndWorkingStatus(events); + + // Status update: working + const workingEvent2 = events[2].result as TaskStatusUpdateEvent; + expect(workingEvent2.kind).toBe('status-update'); + expect(workingEvent2.status.state).toBe('working'); + + // Status update: tool-call-update (validating) + const validatingEvent = events[3].result as TaskStatusUpdateEvent; + expect(validatingEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(validatingEvent.status.message?.parts).toMatchObject([ + { + data: { + status: 'validating', + request: { callId: 'test-call-id-yolo' }, + }, + }, + ]); + + // Status update: tool-call-update (scheduled) + const awaitingEvent = events[4].result as TaskStatusUpdateEvent; + expect(awaitingEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(awaitingEvent.status.message?.parts).toMatchObject([ + { + data: { + status: 'scheduled', + request: { callId: 'test-call-id-yolo' }, + }, + }, + ]); + + // Status update: tool-call-update (executing) + const executingEvent = events[5].result as TaskStatusUpdateEvent; + expect(executingEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(executingEvent.status.message?.parts).toMatchObject([ + { + data: { + status: 'executing', + request: { callId: 'test-call-id-yolo' }, + }, + }, + ]); + + // Status update: tool-call-update (success) + const successEvent = events[6].result as TaskStatusUpdateEvent; + expect(successEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(successEvent.status.message?.parts).toMatchObject([ + { + data: { + status: 'success', + request: { callId: 'test-call-id-yolo' }, + }, + }, + ]); + + // Status update: working (before sending tool result to LLM) + const workingEvent3 = events[7].result as TaskStatusUpdateEvent; + expect(workingEvent3.kind).toBe('status-update'); + expect(workingEvent3.status.state).toBe('working'); + + // Status update: text-content (final LLM response) + const textContentEvent = events[8].result as TaskStatusUpdateEvent; + expect(textContentEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'text-content', + }); + expect(textContentEvent.status.message?.parts).toMatchObject([ + { text: 'Tool executed successfully.' }, + ]); + + assertUniqueFinalEventIsLast(events); + expect(events.length).toBe(10); + }); +}); diff --git a/packages/a2a-server/src/agent.ts b/packages/a2a-server/src/agent.ts new file mode 100644 index 0000000000..501b8cdbbb --- /dev/null +++ b/packages/a2a-server/src/agent.ts @@ -0,0 +1,785 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import express from 'express'; +import { AsyncLocalStorage } from 'node:async_hooks'; + +import type { Message, Task as SDKTask, AgentCard } from '@a2a-js/sdk'; +import type { + TaskStore, + AgentExecutor, + AgentExecutionEvent, + RequestContext, + ExecutionEventBus, +} from '@a2a-js/sdk/server'; +import { DefaultRequestHandler, InMemoryTaskStore } from '@a2a-js/sdk/server'; +import { A2AExpressApp } from '@a2a-js/sdk/server/express'; // Import server components +import type { + ToolCallRequestInfo, + ServerGeminiToolCallRequestEvent, + Config, +} from '@google/gemini-cli-core'; +import { GeminiEventType } from '@google/gemini-cli-core'; +import { v4 as uuidv4 } from 'uuid'; +import { logger } from './logger.js'; +import type { StateChange, AgentSettings } from './types.js'; +import { CoderAgentEvent } from './types.js'; +import { loadConfig, loadEnvironment, setTargetDir } from './config.js'; +import { loadSettings } from './settings.js'; +import { loadExtensions } from './extension.js'; +import { Task } from './task.js'; +import { GCSTaskStore, NoOpTaskStore } from './gcs.js'; +import type { PersistedStateMetadata } from './metadata_types.js'; +import { getPersistedState, setPersistedState } from './metadata_types.js'; + +const requestStorage = new AsyncLocalStorage<{ req: express.Request }>(); + +/** + * Provides a wrapper for Task. Passes data from Task to SDKTask. + * The idea is to use this class inside CoderAgentExecutor to replace Task. + */ +class TaskWrapper { + task: Task; + agentSettings: AgentSettings; + + constructor(task: Task, agentSettings: AgentSettings) { + this.task = task; + this.agentSettings = agentSettings; + } + + get id() { + return this.task.id; + } + + toSDKTask(): SDKTask { + const persistedState: PersistedStateMetadata = { + _agentSettings: this.agentSettings, + _taskState: this.task.taskState, + }; + + const sdkTask: SDKTask = { + id: this.task.id, + contextId: this.task.contextId, + kind: 'task', + status: { + state: this.task.taskState, + timestamp: new Date().toISOString(), + }, + metadata: setPersistedState({}, persistedState), + history: [], + artifacts: [], + }; + sdkTask.metadata!['_contextId'] = this.task.contextId; + return sdkTask; + } +} + +const coderAgentCard: AgentCard = { + name: 'Gemini SDLC Agent', + description: + 'An agent that generates code based on natural language instructions and streams file outputs.', + url: 'http://localhost:41242/', + provider: { + organization: 'Google', + url: 'https://google.com', + }, + protocolVersion: '0.3.0', + version: '0.0.2', // Incremented version + capabilities: { + streaming: true, + pushNotifications: false, + stateTransitionHistory: true, + }, + securitySchemes: undefined, + security: undefined, + defaultInputModes: ['text'], + defaultOutputModes: ['text'], + skills: [ + { + id: 'code_generation', + name: 'Code Generation', + description: + 'Generates code snippets or complete files based on user requests, streaming the results.', + tags: ['code', 'development', 'programming'], + examples: [ + 'Write a python function to calculate fibonacci numbers.', + 'Create an HTML file with a basic button that alerts "Hello!" when clicked.', + ], + inputModes: ['text'], + outputModes: ['text'], + }, + ], + supportsAuthenticatedExtendedCard: false, +}; + +/** + * CoderAgentExecutor implements the agent's core logic for code generation. + */ +class CoderAgentExecutor implements AgentExecutor { + private tasks: Map = new Map(); + // Track tasks with an active execution loop. + private executingTasks = new Set(); + + constructor(private taskStore?: TaskStore) {} + + private async getConfig( + agentSettings: AgentSettings, + taskId: string, + ): Promise { + const workspaceRoot = setTargetDir(agentSettings); + loadEnvironment(); // Will override any global env with workspace envs + const settings = loadSettings(workspaceRoot); + const extensions = loadExtensions(workspaceRoot); + return await loadConfig(settings, extensions, taskId); + } + + /** + * Reconstructs TaskWrapper from SDKTask. + */ + async reconstruct( + sdkTask: SDKTask, + eventBus?: ExecutionEventBus, + ): Promise { + const metadata = sdkTask.metadata || {}; + const persistedState = getPersistedState(metadata); + + if (!persistedState) { + throw new Error( + `Cannot reconstruct task ${sdkTask.id}: missing persisted state in metadata.`, + ); + } + + const agentSettings = persistedState._agentSettings; + const config = await this.getConfig(agentSettings, sdkTask.id); + const contextId = + (metadata['_contextId'] as string) || (sdkTask.contextId as string); + const runtimeTask = await Task.create( + sdkTask.id, + contextId, + config, + eventBus, + ); + runtimeTask.taskState = persistedState._taskState; + await runtimeTask.geminiClient.initialize( + runtimeTask.config.getContentGeneratorConfig(), + ); + + const wrapper = new TaskWrapper(runtimeTask, agentSettings); + this.tasks.set(sdkTask.id, wrapper); + logger.info(`Task ${sdkTask.id} reconstructed from store.`); + return wrapper; + } + + async createTask( + taskId: string, + contextId: string, + agentSettingsInput?: AgentSettings, + eventBus?: ExecutionEventBus, + ): Promise { + const agentSettings = agentSettingsInput || ({} as AgentSettings); + const config = await this.getConfig(agentSettings, taskId); + const runtimeTask = await Task.create(taskId, contextId, config, eventBus); + await runtimeTask.geminiClient.initialize( + runtimeTask.config.getContentGeneratorConfig(), + ); + + const wrapper = new TaskWrapper(runtimeTask, agentSettings); + this.tasks.set(taskId, wrapper); + logger.info(`New task ${taskId} created.`); + return wrapper; + } + + getTask(taskId: string): TaskWrapper | undefined { + return this.tasks.get(taskId); + } + + getAllTasks(): TaskWrapper[] { + return Array.from(this.tasks.values()); + } + + cancelTask = async ( + taskId: string, + eventBus: ExecutionEventBus, + ): Promise => { + logger.info( + `[CoderAgentExecutor] Received cancel request for task ${taskId}`, + ); + const wrapper = this.tasks.get(taskId); + + if (!wrapper) { + logger.warn( + `[CoderAgentExecutor] Task ${taskId} not found for cancellation.`, + ); + eventBus.publish({ + kind: 'status-update', + taskId, + contextId: uuidv4(), + status: { + state: 'failed', + message: { + kind: 'message', + role: 'agent', + parts: [{ kind: 'text', text: `Task ${taskId} not found.` }], + messageId: uuidv4(), + taskId, + }, + }, + final: true, + }); + return; + } + + const { task } = wrapper; + + if (task.taskState === 'canceled' || task.taskState === 'failed') { + logger.info( + `[CoderAgentExecutor] Task ${taskId} is already in a final state: ${task.taskState}. No action needed for cancellation.`, + ); + eventBus.publish({ + kind: 'status-update', + taskId, + contextId: task.contextId, + status: { + state: task.taskState, + message: { + kind: 'message', + role: 'agent', + parts: [ + { + kind: 'text', + text: `Task ${taskId} is already ${task.taskState}.`, + }, + ], + messageId: uuidv4(), + taskId, + }, + }, + final: true, + }); + return; + } + + try { + logger.info( + `[CoderAgentExecutor] Initiating cancellation for task ${taskId}.`, + ); + task.cancelPendingTools('Task canceled by user request.'); + + const stateChange: StateChange = { + kind: CoderAgentEvent.StateChangeEvent, + }; + task.setTaskStateAndPublishUpdate( + 'canceled', + stateChange, + 'Task canceled by user request.', + undefined, + true, + ); + logger.info( + `[CoderAgentExecutor] Task ${taskId} cancellation processed. Saving state.`, + ); + await this.taskStore?.save(wrapper.toSDKTask()); + logger.info(`[CoderAgentExecutor] Task ${taskId} state CANCELED saved.`); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + logger.error( + `[CoderAgentExecutor] Error during task cancellation for ${taskId}: ${errorMessage}`, + error, + ); + eventBus.publish({ + kind: 'status-update', + taskId, + contextId: task.contextId, + status: { + state: 'failed', + message: { + kind: 'message', + role: 'agent', + parts: [ + { + kind: 'text', + text: `Failed to process cancellation for task ${taskId}: ${errorMessage}`, + }, + ], + messageId: uuidv4(), + taskId, + }, + }, + final: true, + }); + } + }; + + async execute( + requestContext: RequestContext, + eventBus: ExecutionEventBus, + ): Promise { + const userMessage = requestContext.userMessage as Message; + const sdkTask = requestContext.task as SDKTask | undefined; + + const taskId = sdkTask?.id || userMessage.taskId || uuidv4(); + const contextId = + userMessage.contextId || + sdkTask?.contextId || + sdkTask?.metadata?.['_contextId'] || + uuidv4(); + + logger.info( + `[CoderAgentExecutor] Executing for taskId: ${taskId}, contextId: ${contextId}`, + ); + logger.info( + `[CoderAgentExecutor] userMessage: ${JSON.stringify(userMessage)}`, + ); + eventBus.on('event', (event: AgentExecutionEvent) => + logger.info('[EventBus event]: ', event), + ); + + const store = requestStorage.getStore(); + if (!store) { + logger.error( + '[CoderAgentExecutor] Could not get request from async local storage. Cancellation on socket close will not be handled for this request.', + ); + } + + const abortController = new AbortController(); + const abortSignal = abortController.signal; + + if (store) { + // Grab the raw socket from the request object + const socket = store.req.socket; + const onClientEnd = () => { + logger.info( + `[CoderAgentExecutor] Client socket closed for task ${taskId}. Cancelling execution.`, + ); + if (!abortController.signal.aborted) { + abortController.abort(); + } + // Clean up the listener to prevent memory leaks + socket.removeListener('close', onClientEnd); + }; + + // Listen on the socket's 'end' event (remote closed the connection) + socket.on('end', onClientEnd); + + // It's also good practice to remove the listener if the task completes successfully + abortSignal.addEventListener('abort', () => { + socket.removeListener('end', onClientEnd); + }); + logger.info( + `[CoderAgentExecutor] Socket close handler set up for task ${taskId}.`, + ); + } + + let wrapper: TaskWrapper | undefined = this.tasks.get(taskId); + + if (wrapper) { + wrapper.task.eventBus = eventBus; + logger.info(`[CoderAgentExecutor] Task ${taskId} found in memory cache.`); + } else if (sdkTask) { + logger.info( + `[CoderAgentExecutor] Task ${taskId} found in TaskStore. Reconstructing...`, + ); + try { + wrapper = await this.reconstruct(sdkTask, eventBus); + } catch (e) { + logger.error( + `[CoderAgentExecutor] Failed to hydrate task ${taskId}:`, + e, + ); + const stateChange: StateChange = { + kind: CoderAgentEvent.StateChangeEvent, + }; + eventBus.publish({ + kind: 'status-update', + taskId, + contextId: sdkTask.contextId, + status: { + state: 'failed', + message: { + kind: 'message', + role: 'agent', + parts: [ + { + kind: 'text', + text: 'Internal error: Task state lost or corrupted.', + }, + ], + messageId: uuidv4(), + taskId, + contextId: sdkTask.contextId, + } as Message, + }, + final: true, + metadata: { coderAgent: stateChange }, + }); + return; + } + } else { + logger.info(`[CoderAgentExecutor] Creating new task ${taskId}.`); + const agentSettings = userMessage.metadata?.[ + 'coderAgent' + ] as AgentSettings; + wrapper = await this.createTask( + taskId, + contextId as string, + agentSettings, + eventBus, + ); + const newTaskSDK = wrapper.toSDKTask(); + eventBus.publish({ + ...newTaskSDK, + kind: 'task', + status: { state: 'submitted', timestamp: new Date().toISOString() }, + history: [userMessage], + }); + try { + await this.taskStore?.save(newTaskSDK); + logger.info(`[CoderAgentExecutor] New task ${taskId} saved to store.`); + } catch (saveError) { + logger.error( + `[CoderAgentExecutor] Failed to save new task ${taskId} to store:`, + saveError, + ); + } + } + + if (!wrapper) { + logger.error( + `[CoderAgentExecutor] Task ${taskId} is unexpectedly undefined after load/create.`, + ); + return; + } + + const currentTask = wrapper.task; + + if (['canceled', 'failed', 'completed'].includes(currentTask.taskState)) { + logger.warn( + `[CoderAgentExecutor] Attempted to execute task ${taskId} which is already in state ${currentTask.taskState}. Ignoring.`, + ); + return; + } + + if (this.executingTasks.has(taskId)) { + logger.info( + `[CoderAgentExecutor] Task ${taskId} has a pending execution. Processing message and yielding.`, + ); + currentTask.eventBus = eventBus; + for await (const _ of currentTask.acceptUserMessage( + requestContext, + abortController.signal, + )) { + logger.info( + `[CoderAgentExecutor] Processing user message ${userMessage.messageId} in secondary execution loop for task ${taskId}.`, + ); + } + // End this execution-- the original/source will be resumed. + return; + } + + logger.info( + `[CoderAgentExecutor] Starting main execution for message ${userMessage.messageId} for task ${taskId}.`, + ); + this.executingTasks.add(taskId); + + try { + let agentTurnActive = true; + logger.info(`[CoderAgentExecutor] Task ${taskId}: Processing user turn.`); + let agentEvents = currentTask.acceptUserMessage( + requestContext, + abortSignal, + ); + + while (agentTurnActive) { + logger.info( + `[CoderAgentExecutor] Task ${taskId}: Processing agent turn (LLM stream).`, + ); + const toolCallRequests: ToolCallRequestInfo[] = []; + for await (const event of agentEvents) { + if (abortSignal.aborted) { + logger.warn( + `[CoderAgentExecutor] Task ${taskId}: Abort signal received during agent event processing.`, + ); + throw new Error('Execution aborted'); + } + if (event.type === GeminiEventType.ToolCallRequest) { + toolCallRequests.push( + (event as ServerGeminiToolCallRequestEvent).value, + ); + continue; + } + await currentTask.acceptAgentMessage(event); + } + + if (abortSignal.aborted) throw new Error('Execution aborted'); + + if (toolCallRequests.length > 0) { + logger.info( + `[CoderAgentExecutor] Task ${taskId}: Found ${toolCallRequests.length} tool call requests. Scheduling as a batch.`, + ); + await currentTask.scheduleToolCalls(toolCallRequests, abortSignal); + } + + logger.info( + `[CoderAgentExecutor] Task ${taskId}: Waiting for pending tools if any.`, + ); + await currentTask.waitForPendingTools(); + logger.info( + `[CoderAgentExecutor] Task ${taskId}: All pending tools completed or none were pending.`, + ); + + if (abortSignal.aborted) throw new Error('Execution aborted'); + + const completedTools = currentTask.getAndClearCompletedTools(); + + if (completedTools.length > 0) { + // If all completed tool calls were canceled, manually add them to history and set state to input-required, final:true + if (completedTools.every((tool) => tool.status === 'cancelled')) { + logger.info( + `[CoderAgentExecutor] Task ${taskId}: All tool calls were cancelled. Updating history and ending agent turn.`, + ); + currentTask.addToolResponsesToHistory(completedTools); + agentTurnActive = false; + const stateChange: StateChange = { + kind: CoderAgentEvent.StateChangeEvent, + }; + currentTask.setTaskStateAndPublishUpdate( + 'input-required', + stateChange, + undefined, + undefined, + true, + ); + } else { + logger.info( + `[CoderAgentExecutor] Task ${taskId}: Found ${completedTools.length} completed tool calls. Sending results back to LLM.`, + ); + + agentEvents = currentTask.sendCompletedToolsToLlm( + completedTools, + abortSignal, + ); + // Continue the loop to process the LLM response to the tool results. + } + } else { + logger.info( + `[CoderAgentExecutor] Task ${taskId}: No more tool calls to process. Ending agent turn.`, + ); + agentTurnActive = false; + } + } + + logger.info( + `[CoderAgentExecutor] Task ${taskId}: Agent turn finished, setting to input-required.`, + ); + const stateChange: StateChange = { + kind: CoderAgentEvent.StateChangeEvent, + }; + currentTask.setTaskStateAndPublishUpdate( + 'input-required', + stateChange, + undefined, + undefined, + true, + ); + } catch (error) { + if (abortSignal.aborted) { + logger.warn(`[CoderAgentExecutor] Task ${taskId} execution aborted.`); + currentTask.cancelPendingTools('Execution aborted'); + if ( + currentTask.taskState !== 'canceled' && + currentTask.taskState !== 'failed' + ) { + currentTask.setTaskStateAndPublishUpdate( + 'input-required', + { kind: CoderAgentEvent.StateChangeEvent }, + 'Execution aborted by client.', + undefined, + true, + ); + } + } else { + const errorMessage = + error instanceof Error ? error.message : 'Agent execution error'; + logger.error( + `[CoderAgentExecutor] Error executing agent for task ${taskId}:`, + error, + ); + currentTask.cancelPendingTools(errorMessage); + if (currentTask.taskState !== 'failed') { + const stateChange: StateChange = { + kind: CoderAgentEvent.StateChangeEvent, + }; + currentTask.setTaskStateAndPublishUpdate( + 'failed', + stateChange, + errorMessage, + undefined, + true, + ); + } + } + } finally { + this.executingTasks.delete(taskId); + logger.info( + `[CoderAgentExecutor] Saving final state for task ${taskId}.`, + ); + try { + await this.taskStore?.save(wrapper.toSDKTask()); + logger.info(`[CoderAgentExecutor] Task ${taskId} state saved.`); + } catch (saveError) { + logger.error( + `[CoderAgentExecutor] Failed to save task ${taskId} state in finally block:`, + saveError, + ); + } + } + } +} + +export function updateCoderAgentCardUrl(port: number) { + coderAgentCard.url = `http://localhost:${port}/`; +} + +export async function main() { + try { + const expressApp = await createApp(); + const port = process.env['CODER_AGENT_PORT'] || 0; + + const server = expressApp.listen(port, () => { + const address = server.address(); + let actualPort; + if (process.env['CODER_AGENT_PORT']) { + actualPort = process.env['CODER_AGENT_PORT']; + } else if (address && typeof address !== 'string') { + actualPort = address.port; + } else { + throw new Error('[Core Agent] Could not find port number.'); + } + updateCoderAgentCardUrl(Number(actualPort)); + logger.info( + `[CoreAgent] Agent Server started on http://localhost:${actualPort}`, + ); + logger.info( + `[CoreAgent] Agent Card: http://localhost:${actualPort}/.well-known/agent-card.json`, + ); + logger.info('[CoreAgent] Press Ctrl+C to stop the server'); + }); + } catch (error) { + logger.error('[CoreAgent] Error during startup:', error); + process.exit(1); + } +} + +export async function createApp() { + try { + // loadEnvironment() is called within getConfig now + const bucketName = process.env['GCS_BUCKET_NAME']; + let taskStoreForExecutor: TaskStore; + let taskStoreForHandler: TaskStore; + + if (bucketName) { + logger.info(`Using GCSTaskStore with bucket: ${bucketName}`); + const gcsTaskStore = new GCSTaskStore(bucketName); + taskStoreForExecutor = gcsTaskStore; + taskStoreForHandler = new NoOpTaskStore(gcsTaskStore); + } else { + logger.info('Using InMemoryTaskStore'); + const inMemoryTaskStore = new InMemoryTaskStore(); + taskStoreForExecutor = inMemoryTaskStore; + taskStoreForHandler = inMemoryTaskStore; + } + + const agentExecutor = new CoderAgentExecutor(taskStoreForExecutor); + + const requestHandler = new DefaultRequestHandler( + coderAgentCard, + taskStoreForHandler, + agentExecutor, + ); + + let expressApp = express(); + expressApp.use((req, res, next) => { + requestStorage.run({ req }, next); + }); + + const appBuilder = new A2AExpressApp(requestHandler); + expressApp = appBuilder.setupRoutes(expressApp, ''); + expressApp.use(express.json()); + + expressApp.post('/tasks', async (req, res) => { + try { + const taskId = uuidv4(); + const agentSettings = req.body.agentSettings as + | AgentSettings + | undefined; + const contextId = req.body.contextId || uuidv4(); + const wrapper = await agentExecutor.createTask( + taskId, + contextId, + agentSettings, + ); + await taskStoreForExecutor.save(wrapper.toSDKTask()); + res.status(201).json(wrapper.id); + } catch (error) { + logger.error('[CoreAgent] Error creating task:', error); + const errorMessage = + error instanceof Error + ? error.message + : 'Unknown error creating task'; + res.status(500).send({ error: errorMessage }); + } + }); + + expressApp.get('/tasks/metadata', async (req, res) => { + // This endpoint is only meaningful if the task store is in-memory. + if (!(taskStoreForExecutor instanceof InMemoryTaskStore)) { + res.status(501).send({ + error: + 'Listing all task metadata is only supported when using InMemoryTaskStore.', + }); + } + try { + const wrappers = agentExecutor.getAllTasks(); + if (wrappers && wrappers.length > 0) { + const tasksMetadata = await Promise.all( + wrappers.map((wrapper) => wrapper.task.getMetadata()), + ); + res.status(200).json(tasksMetadata); + } else { + res.status(204).send(); + } + } catch (error) { + logger.error('[CoreAgent] Error getting all task metadata:', error); + const errorMessage = + error instanceof Error + ? error.message + : 'Unknown error getting task metadata'; + res.status(500).send({ error: errorMessage }); + } + }); + + expressApp.get('/tasks/:taskId/metadata', async (req, res) => { + const taskId = req.params.taskId; + let wrapper = agentExecutor.getTask(taskId); + if (!wrapper) { + const sdkTask = await taskStoreForExecutor.load(taskId); + if (sdkTask) { + wrapper = await agentExecutor.reconstruct(sdkTask); + } + } + if (!wrapper) { + res.status(404).send({ error: 'Task not found' }); + return; + } + res.json({ metadata: await wrapper.task.getMetadata() }); + }); + return expressApp; + } catch (error) { + logger.error('[CoreAgent] Error during startup:', error); + process.exit(1); + } +} diff --git a/packages/a2a-server/src/config.ts b/packages/a2a-server/src/config.ts new file mode 100644 index 0000000000..0aacdbe15f --- /dev/null +++ b/packages/a2a-server/src/config.ts @@ -0,0 +1,203 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { homedir } from 'node:os'; +import * as dotenv from 'dotenv'; + +import type { TelemetryTarget } from '@google/gemini-cli-core'; +import { + AuthType, + Config, + type ConfigParameters, + FileDiscoveryService, + ApprovalMode, + loadServerHierarchicalMemory, + GEMINI_CONFIG_DIR, + DEFAULT_GEMINI_EMBEDDING_MODEL, + DEFAULT_GEMINI_MODEL, +} from '@google/gemini-cli-core'; + +import { logger } from './logger.js'; +import type { Settings } from './settings.js'; +import type { Extension } from './extension.js'; +import { type AgentSettings, CoderAgentEvent } from './types.js'; + +export async function loadConfig( + settings: Settings, + extensions: Extension[], + taskId: string, +): Promise { + const mcpServers = mergeMcpServers(settings, extensions); + const workspaceDir = process.cwd(); + const adcFilePath = process.env['GOOGLE_APPLICATION_CREDENTIALS']; + + const configParams: ConfigParameters = { + sessionId: taskId, + model: DEFAULT_GEMINI_MODEL, + embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL, + sandbox: undefined, // Sandbox might not be relevant for a server-side agent + targetDir: workspaceDir, // Or a specific directory the agent operates on + debugMode: process.env['DEBUG'] === 'true' || false, + question: '', // Not used in server mode directly like CLI + fullContext: false, // Server might have different context needs + coreTools: settings.coreTools || undefined, + excludeTools: settings.excludeTools || undefined, + showMemoryUsage: settings.showMemoryUsage || false, + approvalMode: + process.env['GEMINI_YOLO_MODE'] === 'true' + ? ApprovalMode.YOLO + : ApprovalMode.DEFAULT, + mcpServers, + cwd: workspaceDir, + telemetry: { + enabled: settings.telemetry?.enabled, + target: settings.telemetry?.target as TelemetryTarget, + otlpEndpoint: + process.env['OTEL_EXPORTER_OTLP_ENDPOINT'] ?? + settings.telemetry?.otlpEndpoint, + logPrompts: settings.telemetry?.logPrompts, + }, + // Git-aware file filtering settings + fileFiltering: { + respectGitIgnore: settings.fileFiltering?.respectGitIgnore, + enableRecursiveFileSearch: + settings.fileFiltering?.enableRecursiveFileSearch, + }, + ideMode: false, + }; + + const fileService = new FileDiscoveryService(workspaceDir); + const extensionContextFilePaths = extensions.flatMap((e) => e.contextFiles); + const { memoryContent, fileCount } = await loadServerHierarchicalMemory( + workspaceDir, + [workspaceDir], + false, + fileService, + extensionContextFilePaths, + ); + configParams.userMemory = memoryContent; + configParams.geminiMdFileCount = fileCount; + + const config = new Config({ + ...configParams, + }); + // Needed to initialize ToolRegistry, and git checkpointing if enabled + await config.initialize(); + + if (process.env['USE_CCPA']) { + logger.info('[Config] Using CCPA Auth:'); + try { + if (adcFilePath) { + path.resolve(adcFilePath); + } + } catch (e) { + logger.error( + `[Config] USE_CCPA env var is true but unable to resolve GOOGLE_APPLICATION_CREDENTIALS file path ${adcFilePath}. Error ${e}`, + ); + } + await config.refreshAuth(AuthType.LOGIN_WITH_GOOGLE); + logger.info( + `[Config] GOOGLE_CLOUD_PROJECT: ${process.env['GOOGLE_CLOUD_PROJECT']}`, + ); + } else if (process.env['GEMINI_API_KEY']) { + logger.info('[Config] Using Gemini API Key'); + await config.refreshAuth(AuthType.USE_GEMINI); + } else { + logger.error( + `[Config] Unable to set GeneratorConfig. Please provide a GEMINI_API_KEY or set USE_CCPA.`, + ); + } + + return config; +} + +export function mergeMcpServers(settings: Settings, extensions: Extension[]) { + const mcpServers = { ...(settings.mcpServers || {}) }; + for (const extension of extensions) { + Object.entries(extension.config.mcpServers || {}).forEach( + ([key, server]) => { + if (mcpServers[key]) { + console.warn( + `Skipping extension MCP config for server with key "${key}" as it already exists.`, + ); + return; + } + mcpServers[key] = server; + }, + ); + } + return mcpServers; +} + +export function setTargetDir(agentSettings: AgentSettings | undefined): string { + const originalCWD = process.cwd(); + const targetDir = + process.env['CODER_AGENT_WORKSPACE_PATH'] ?? + (agentSettings?.kind === CoderAgentEvent.StateAgentSettingsEvent + ? agentSettings.workspacePath + : undefined); + + if (!targetDir) { + return originalCWD; + } + + logger.info( + `[CoderAgentExecutor] Overriding workspace path to: ${targetDir}`, + ); + + try { + const resolvedPath = path.resolve(targetDir); + process.chdir(resolvedPath); + return resolvedPath; + } catch (e) { + logger.error( + `[CoderAgentExecutor] Error resolving workspace path: ${e}, returning original os.cwd()`, + ); + return originalCWD; + } +} + +export function loadEnvironment(): void { + const envFilePath = findEnvFile(process.cwd()); + if (envFilePath) { + dotenv.config({ path: envFilePath, override: true }); + } +} + +function findEnvFile(startDir: string): string | null { + let currentDir = path.resolve(startDir); + while (true) { + // prefer gemini-specific .env under GEMINI_DIR + const geminiEnvPath = path.join(currentDir, GEMINI_CONFIG_DIR, '.env'); + if (fs.existsSync(geminiEnvPath)) { + return geminiEnvPath; + } + const envPath = path.join(currentDir, '.env'); + if (fs.existsSync(envPath)) { + return envPath; + } + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir || !parentDir) { + // check .env under home as fallback, again preferring gemini-specific .env + const homeGeminiEnvPath = path.join( + process.cwd(), + GEMINI_CONFIG_DIR, + '.env', + ); + if (fs.existsSync(homeGeminiEnvPath)) { + return homeGeminiEnvPath; + } + const homeEnvPath = path.join(homedir(), '.env'); + if (fs.existsSync(homeEnvPath)) { + return homeEnvPath; + } + return null; + } + currentDir = parentDir; + } +} diff --git a/packages/a2a-server/src/endpoints.test.ts b/packages/a2a-server/src/endpoints.test.ts new file mode 100644 index 0000000000..77a1e59ac0 --- /dev/null +++ b/packages/a2a-server/src/endpoints.test.ts @@ -0,0 +1,146 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; +import request from 'supertest'; +import type express from 'express'; +import { createApp, updateCoderAgentCardUrl } from './agent.js'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import * as os from 'node:os'; +import type { Server } from 'node:http'; +import type { TaskMetadata } from './types.js'; +import type { AddressInfo } from 'node:net'; + +// Mock the logger to avoid polluting test output +// Comment out to help debug +vi.mock('./logger.js', () => ({ + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, +})); + +// Mock Task.create to avoid its complex setup +vi.mock('./task.js', () => { + class MockTask { + id: string; + contextId: string; + taskState = 'submitted'; + config = { + getContentGeneratorConfig: vi + .fn() + .mockReturnValue({ model: 'gemini-pro' }), + }; + geminiClient = { + initialize: vi.fn().mockResolvedValue(undefined), + }; + constructor(id: string, contextId: string) { + this.id = id; + this.contextId = contextId; + } + static create = vi + .fn() + .mockImplementation((id, contextId) => + Promise.resolve(new MockTask(id, contextId)), + ); + getMetadata = vi.fn().mockImplementation(async () => ({ + id: this.id, + contextId: this.contextId, + taskState: this.taskState, + model: 'gemini-pro', + mcpServers: [], + availableTools: [], + })); + } + return { Task: MockTask }; +}); + +describe('Agent Server Endpoints', () => { + let app: express.Express; + let server: Server; + let testWorkspace: string; + + const createTask = (contextId: string) => + request(app) + .post('/tasks') + .send({ + contextId, + agentSettings: { + kind: 'agent-settings', + workspacePath: testWorkspace, + }, + }) + .set('Content-Type', 'application/json'); + + beforeAll(async () => { + // Create a unique temporary directory for the workspace to avoid conflicts + testWorkspace = fs.mkdtempSync( + path.join(os.tmpdir(), 'gemini-agent-test-'), + ); + app = await createApp(); + await new Promise((resolve) => { + server = app.listen(0, () => { + const port = (server.address() as AddressInfo).port; + updateCoderAgentCardUrl(port); + resolve(); + }); + }); + }); + + afterAll( + () => + new Promise((resolve, reject) => { + server.close((err) => { + if (err) return reject(err); + + try { + fs.rmSync(testWorkspace, { recursive: true, force: true }); + } catch (e) { + console.warn(`Could not remove temp dir '${testWorkspace}':`, e); + } + resolve(); + }); + }), + ); + + it('should create a new task via POST /tasks', async () => { + const response = await createTask('test-context'); + expect(response.status).toBe(201); + expect(response.body).toBeTypeOf('string'); // Should return the task ID + }, 7000); + + it('should get metadata for a specific task via GET /tasks/:taskId/metadata', async () => { + const createResponse = await createTask('test-context-2'); + const taskId = createResponse.body; + const response = await request(app).get(`/tasks/${taskId}/metadata`); + expect(response.status).toBe(200); + expect(response.body.metadata.id).toBe(taskId); + }, 6000); + + it('should get metadata for all tasks via GET /tasks/metadata', async () => { + const createResponse = await createTask('test-context-3'); + const taskId = createResponse.body; + const response = await request(app).get('/tasks/metadata'); + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + expect(response.body.length).toBeGreaterThan(0); + const taskMetadata = response.body.find( + (m: TaskMetadata) => m.id === taskId, + ); + expect(taskMetadata).toBeDefined(); + }); + + it('should return 404 for a non-existent task', async () => { + const response = await request(app).get('/tasks/fake-task/metadata'); + expect(response.status).toBe(404); + }); + + it('should return agent metadata via GET /.well-known/agent-card.json', async () => { + const response = await request(app).get('/.well-known/agent-card.json'); + const port = (server.address() as AddressInfo).port; + expect(response.status).toBe(200); + expect(response.body.name).toBe('Gemini SDLC Agent'); + expect(response.body.url).toBe(`http://localhost:${port}/`); + }); +}); diff --git a/packages/a2a-server/src/extension.ts b/packages/a2a-server/src/extension.ts new file mode 100644 index 0000000000..3a33eebe3d --- /dev/null +++ b/packages/a2a-server/src/extension.ts @@ -0,0 +1,118 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Copied exactly from packages/cli/src/config/extension.ts, last PR #1026 + +import type { MCPServerConfig } from '@google/gemini-cli-core'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import * as os from 'node:os'; +import { logger } from './logger.js'; + +export const EXTENSIONS_DIRECTORY_NAME = path.join('.gemini', 'extensions'); +export const EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json'; + +export interface Extension { + config: ExtensionConfig; + contextFiles: string[]; +} + +export interface ExtensionConfig { + name: string; + version: string; + mcpServers?: Record; + contextFileName?: string | string[]; +} + +export function loadExtensions(workspaceDir: string): Extension[] { + const allExtensions = [ + ...loadExtensionsFromDir(workspaceDir), + ...loadExtensionsFromDir(os.homedir()), + ]; + + const uniqueExtensions: Extension[] = []; + const seenNames = new Set(); + for (const extension of allExtensions) { + if (!seenNames.has(extension.config.name)) { + logger.info( + `Loading extension: ${extension.config.name} (version: ${extension.config.version})`, + ); + uniqueExtensions.push(extension); + seenNames.add(extension.config.name); + } + } + + return uniqueExtensions; +} + +function loadExtensionsFromDir(dir: string): Extension[] { + const extensionsDir = path.join(dir, EXTENSIONS_DIRECTORY_NAME); + if (!fs.existsSync(extensionsDir)) { + return []; + } + + const extensions: Extension[] = []; + for (const subdir of fs.readdirSync(extensionsDir)) { + const extensionDir = path.join(extensionsDir, subdir); + + const extension = loadExtension(extensionDir); + if (extension != null) { + extensions.push(extension); + } + } + return extensions; +} + +function loadExtension(extensionDir: string): Extension | null { + if (!fs.statSync(extensionDir).isDirectory()) { + logger.error( + `Warning: unexpected file ${extensionDir} in extensions directory.`, + ); + return null; + } + + const configFilePath = path.join(extensionDir, EXTENSIONS_CONFIG_FILENAME); + if (!fs.existsSync(configFilePath)) { + logger.error( + `Warning: extension directory ${extensionDir} does not contain a config file ${configFilePath}.`, + ); + return null; + } + + try { + const configContent = fs.readFileSync(configFilePath, 'utf-8'); + const config = JSON.parse(configContent) as ExtensionConfig; + if (!config.name || !config.version) { + logger.error( + `Invalid extension config in ${configFilePath}: missing name or version.`, + ); + return null; + } + + const contextFiles = getContextFileNames(config) + .map((contextFileName) => path.join(extensionDir, contextFileName)) + .filter((contextFilePath) => fs.existsSync(contextFilePath)); + + return { + config, + contextFiles, + }; + } catch (e) { + logger.error( + `Warning: error parsing extension config in ${configFilePath}: ${e}`, + ); + return null; + } +} + +function getContextFileNames(config: ExtensionConfig): string[] { + if (!config.contextFileName) { + return ['GEMINI.md']; + } else if (!Array.isArray(config.contextFileName)) { + return [config.contextFileName]; + } + return config.contextFileName; +} diff --git a/packages/a2a-server/src/gcs.test.ts b/packages/a2a-server/src/gcs.test.ts new file mode 100644 index 0000000000..3553ccc6cc --- /dev/null +++ b/packages/a2a-server/src/gcs.test.ts @@ -0,0 +1,340 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Storage } from '@google-cloud/storage'; +import * as fse from 'fs-extra'; +import { promises as fsPromises, createReadStream } from 'node:fs'; +import * as tar from 'tar'; +import { gzipSync, gunzipSync } from 'node:zlib'; +import { v4 as uuidv4 } from 'uuid'; +import type { Task as SDKTask } from '@a2a-js/sdk'; +import type { TaskStore } from '@a2a-js/sdk/server'; +import type { Mocked, MockedClass, Mock } from 'vitest'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +import { GCSTaskStore, NoOpTaskStore } from './gcs.js'; +import { logger } from './logger.js'; +import * as configModule from './config.js'; +import * as metadataModule from './metadata_types.js'; + +// Mock dependencies +vi.mock('@google-cloud/storage'); +vi.mock('fs-extra', () => ({ + pathExists: vi.fn(), + readdir: vi.fn(), + remove: vi.fn(), + ensureDir: vi.fn(), +})); +vi.mock('node:fs', async () => { + const actual = await vi.importActual('node:fs'); + return { + ...actual, + promises: { + ...actual.promises, + readdir: vi.fn(), + }, + createReadStream: vi.fn(), + }; +}); +vi.mock('tar'); +vi.mock('zlib'); +vi.mock('uuid'); +vi.mock('./logger', () => ({ + logger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }, +})); +vi.mock('./config'); +vi.mock('./metadata_types'); +vi.mock('node:stream/promises', () => ({ + pipeline: vi.fn(), +})); + +const mockStorage = Storage as MockedClass; +const mockFse = fse as Mocked; +const mockCreateReadStream = createReadStream as Mock; +const mockTar = tar as Mocked; +const mockGzipSync = gzipSync as Mock; +const mockGunzipSync = gunzipSync as Mock; +const mockUuidv4 = uuidv4 as Mock; +const mockSetTargetDir = configModule.setTargetDir as Mock; +const mockGetPersistedState = metadataModule.getPersistedState as Mock; +const METADATA_KEY = metadataModule.METADATA_KEY || '__persistedState'; + +type MockWriteStream = { + on: Mock< + (event: string, cb: (error?: Error | null) => void) => MockWriteStream + >; + destroy: Mock<() => void>; + destroyed: boolean; +}; + +type MockFile = { + save: Mock<(data: Buffer | string) => Promise>; + download: Mock<() => Promise<[Buffer]>>; + exists: Mock<() => Promise<[boolean]>>; + createWriteStream: Mock<() => MockWriteStream>; +}; + +type MockBucket = { + exists: Mock<() => Promise<[boolean]>>; + file: Mock<(path: string) => MockFile>; + name: string; +}; + +type MockStorageInstance = { + bucket: Mock<(name: string) => MockBucket>; + getBuckets: Mock<() => Promise<[Array<{ name: string }>]>>; + createBucket: Mock<(name: string) => Promise<[MockBucket]>>; +}; + +describe('GCSTaskStore', () => { + let bucketName: string; + let mockBucket: MockBucket; + let mockFile: MockFile; + let mockWriteStream: MockWriteStream; + let mockStorageInstance: MockStorageInstance; + + beforeEach(() => { + vi.clearAllMocks(); + bucketName = 'test-bucket'; + + mockWriteStream = { + on: vi.fn((event, cb) => { + if (event === 'finish') setTimeout(cb, 0); // Simulate async finish + return mockWriteStream; + }), + destroy: vi.fn(), + destroyed: false, + }; + + mockFile = { + save: vi.fn().mockResolvedValue(undefined), + download: vi.fn().mockResolvedValue([Buffer.from('')]), + exists: vi.fn().mockResolvedValue([true]), + createWriteStream: vi.fn().mockReturnValue(mockWriteStream), + }; + + mockBucket = { + exists: vi.fn().mockResolvedValue([true]), + file: vi.fn().mockReturnValue(mockFile), + name: bucketName, + }; + + mockStorageInstance = { + bucket: vi.fn().mockReturnValue(mockBucket), + getBuckets: vi.fn().mockResolvedValue([[{ name: bucketName }]]), + createBucket: vi.fn().mockResolvedValue([mockBucket]), + }; + mockStorage.mockReturnValue(mockStorageInstance as unknown as Storage); + + mockUuidv4.mockReturnValue('test-uuid'); + mockSetTargetDir.mockReturnValue('/tmp/workdir'); + mockGetPersistedState.mockReturnValue({ + _agentSettings: {}, + _taskState: 'submitted', + }); + (fse.pathExists as Mock).mockResolvedValue(true); + (fsPromises.readdir as Mock).mockResolvedValue(['file1.txt']); + mockTar.c.mockResolvedValue(undefined); + mockTar.x.mockResolvedValue(undefined); + mockFse.remove.mockResolvedValue(undefined); + mockFse.ensureDir.mockResolvedValue(undefined); + mockGzipSync.mockReturnValue(Buffer.from('compressed')); + mockGunzipSync.mockReturnValue(Buffer.from('{}')); + mockCreateReadStream.mockReturnValue({ on: vi.fn(), pipe: vi.fn() }); + }); + + describe('Constructor & Initialization', () => { + it('should initialize and check bucket existence', async () => { + const store = new GCSTaskStore(bucketName); + await store['ensureBucketInitialized'](); + expect(mockStorage).toHaveBeenCalledTimes(1); + expect(mockStorageInstance.getBuckets).toHaveBeenCalled(); + expect(logger.info).toHaveBeenCalledWith( + expect.stringContaining('Bucket test-bucket exists'), + ); + }); + + it('should create bucket if it does not exist', async () => { + mockStorageInstance.getBuckets.mockResolvedValue([[]]); + const store = new GCSTaskStore(bucketName); + await store['ensureBucketInitialized'](); + expect(mockStorageInstance.createBucket).toHaveBeenCalledWith(bucketName); + expect(logger.info).toHaveBeenCalledWith( + expect.stringContaining('Bucket test-bucket created successfully'), + ); + }); + + it('should throw if bucket creation fails', async () => { + mockStorageInstance.getBuckets.mockResolvedValue([[]]); + mockStorageInstance.createBucket.mockRejectedValue( + new Error('Create failed'), + ); + const store = new GCSTaskStore(bucketName); + await expect(store['ensureBucketInitialized']()).rejects.toThrow( + 'Failed to create GCS bucket test-bucket: Error: Create failed', + ); + }); + }); + + describe('save', () => { + const mockTask: SDKTask = { + id: 'task1', + contextId: 'ctx1', + kind: 'task', + status: { state: 'working' }, + metadata: {}, + }; + + it('should save metadata and workspace', async () => { + const store = new GCSTaskStore(bucketName); + await store.save(mockTask); + + expect(mockFile.save).toHaveBeenCalledTimes(1); + expect(mockTar.c).toHaveBeenCalledTimes(1); + expect(mockCreateReadStream).toHaveBeenCalledTimes(1); + expect(mockFse.remove).toHaveBeenCalledTimes(1); + expect(logger.info).toHaveBeenCalledWith( + expect.stringContaining('metadata saved to GCS'), + ); + expect(logger.info).toHaveBeenCalledWith( + expect.stringContaining('workspace saved to GCS'), + ); + }); + + it('should handle tar creation failure', async () => { + mockFse.pathExists.mockImplementation( + async (path) => + !path.toString().includes('task-task1-workspace-test-uuid.tar.gz'), + ); + const store = new GCSTaskStore(bucketName); + await expect(store.save(mockTask)).rejects.toThrow( + 'tar.c command failed to create', + ); + }); + }); + + describe('load', () => { + it('should load task metadata and workspace', async () => { + mockGunzipSync.mockReturnValue( + Buffer.from( + JSON.stringify({ + [METADATA_KEY]: { _agentSettings: {}, _taskState: 'submitted' }, + _contextId: 'ctx1', + }), + ), + ); + mockFile.download.mockResolvedValue([Buffer.from('compressed metadata')]); + mockFile.download.mockResolvedValueOnce([ + Buffer.from('compressed metadata'), + ]); + mockBucket.file = vi.fn((path) => { + const newMockFile = { ...mockFile }; + if (path.includes('metadata')) { + newMockFile.download = vi + .fn() + .mockResolvedValue([Buffer.from('compressed metadata')]); + newMockFile.exists = vi.fn().mockResolvedValue([true]); + } else { + newMockFile.download = vi + .fn() + .mockResolvedValue([Buffer.from('compressed workspace')]); + newMockFile.exists = vi.fn().mockResolvedValue([true]); + } + return newMockFile; + }); + + const store = new GCSTaskStore(bucketName); + const task = await store.load('task1'); + + expect(task).toBeDefined(); + expect(task?.id).toBe('task1'); + expect(mockBucket.file).toHaveBeenCalledWith( + 'tasks/task1/metadata.tar.gz', + ); + expect(mockBucket.file).toHaveBeenCalledWith( + 'tasks/task1/workspace.tar.gz', + ); + expect(mockTar.x).toHaveBeenCalledTimes(1); + expect(mockFse.remove).toHaveBeenCalledTimes(1); + }); + + it('should return undefined if metadata not found', async () => { + mockFile.exists.mockResolvedValue([false]); + const store = new GCSTaskStore(bucketName); + const task = await store.load('task1'); + expect(task).toBeUndefined(); + expect(mockBucket.file).toHaveBeenCalledWith( + 'tasks/task1/metadata.tar.gz', + ); + }); + + it('should load metadata even if workspace not found', async () => { + mockGunzipSync.mockReturnValue( + Buffer.from( + JSON.stringify({ + [METADATA_KEY]: { _agentSettings: {}, _taskState: 'submitted' }, + _contextId: 'ctx1', + }), + ), + ); + + mockBucket.file = vi.fn((path) => { + const newMockFile = { ...mockFile }; + if (path.includes('workspace.tar.gz')) { + newMockFile.exists = vi.fn().mockResolvedValue([false]); + } else { + newMockFile.exists = vi.fn().mockResolvedValue([true]); + newMockFile.download = vi + .fn() + .mockResolvedValue([Buffer.from('compressed metadata')]); + } + return newMockFile; + }); + + const store = new GCSTaskStore(bucketName); + const task = await store.load('task1'); + + expect(task).toBeDefined(); + expect(mockTar.x).not.toHaveBeenCalled(); + expect(logger.info).toHaveBeenCalledWith( + expect.stringContaining('workspace archive not found'), + ); + }); + }); +}); + +describe('NoOpTaskStore', () => { + let realStore: TaskStore; + let noOpStore: NoOpTaskStore; + + beforeEach(() => { + // Create a mock of the real store to delegate to + realStore = { + save: vi.fn(), + load: vi.fn().mockResolvedValue({ id: 'task-123' } as SDKTask), + }; + noOpStore = new NoOpTaskStore(realStore); + }); + + it("should not call the real store's save method", async () => { + const mockTask: SDKTask = { id: 'test-task' } as SDKTask; + await noOpStore.save(mockTask); + expect(realStore.save).not.toHaveBeenCalled(); + }); + + it('should delegate the load method to the real store', async () => { + const taskId = 'task-123'; + const result = await noOpStore.load(taskId); + expect(realStore.load).toHaveBeenCalledWith(taskId); + expect(result).toBeDefined(); + expect(result?.id).toBe(taskId); + }); +}); diff --git a/packages/a2a-server/src/gcs.ts b/packages/a2a-server/src/gcs.ts new file mode 100644 index 0000000000..8591d45462 --- /dev/null +++ b/packages/a2a-server/src/gcs.ts @@ -0,0 +1,308 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Storage } from '@google-cloud/storage'; +import { gzipSync, gunzipSync } from 'node:zlib'; +import * as tar from 'tar'; +import * as fse from 'fs-extra'; +import { promises as fsPromises, createReadStream } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import type { Task as SDKTask } from '@a2a-js/sdk'; +import type { TaskStore } from '@a2a-js/sdk/server'; +import { logger } from './logger.js'; +import { setTargetDir } from './config.js'; +import { + getPersistedState, + type PersistedTaskMetadata, +} from './metadata_types.js'; +import { v4 as uuidv4 } from 'uuid'; + +type ObjectType = 'metadata' | 'workspace'; + +const getTmpArchiveFilename = (taskId: string): string => + `task-${taskId}-workspace-${uuidv4()}.tar.gz`; + +export class GCSTaskStore implements TaskStore { + private storage: Storage; + private bucketName: string; + private bucketInitialized: Promise; + + constructor(bucketName: string) { + if (!bucketName) { + throw new Error('GCS bucket name is required.'); + } + this.storage = new Storage(); + this.bucketName = bucketName; + logger.info(`GCSTaskStore initializing with bucket: ${this.bucketName}`); + // Prerequisites: user account or service account must have storage admin IAM role + // and the bucket name must be unique. + this.bucketInitialized = this.initializeBucket(); + } + + private async initializeBucket(): Promise { + try { + const [buckets] = await this.storage.getBuckets(); + const exists = buckets.some((bucket) => bucket.name === this.bucketName); + + if (!exists) { + logger.info( + `Bucket ${this.bucketName} does not exist in the list. Attempting to create...`, + ); + try { + await this.storage.createBucket(this.bucketName); + logger.info(`Bucket ${this.bucketName} created successfully.`); + } catch (createError) { + logger.info( + `Failed to create bucket ${this.bucketName}: ${createError}`, + ); + throw new Error( + `Failed to create GCS bucket ${this.bucketName}: ${createError}`, + ); + } + } else { + logger.info(`Bucket ${this.bucketName} exists.`); + } + } catch (error) { + logger.info( + `Error during bucket initialization for ${this.bucketName}: ${error}`, + ); + throw new Error( + `Failed to initialize GCS bucket ${this.bucketName}: ${error}`, + ); + } + } + + private async ensureBucketInitialized(): Promise { + await this.bucketInitialized; + } + + private getObjectPath(taskId: string, type: ObjectType): string { + return `tasks/${taskId}/${type}.tar.gz`; + } + + async save(task: SDKTask): Promise { + await this.ensureBucketInitialized(); + const taskId = task.id; + const persistedState = getPersistedState( + task.metadata as PersistedTaskMetadata, + ); + + if (!persistedState) { + throw new Error(`Task ${taskId} is missing persisted state in metadata.`); + } + const workDir = process.cwd(); + + const metadataObjectPath = this.getObjectPath(taskId, 'metadata'); + const workspaceObjectPath = this.getObjectPath(taskId, 'workspace'); + + const dataToStore = task.metadata; + + try { + const jsonString = JSON.stringify(dataToStore); + const compressedMetadata = gzipSync(Buffer.from(jsonString)); + const metadataFile = this.storage + .bucket(this.bucketName) + .file(metadataObjectPath); + await metadataFile.save(compressedMetadata, { + contentType: 'application/gzip', + }); + logger.info( + `Task ${taskId} metadata saved to GCS: gs://${this.bucketName}/${metadataObjectPath}`, + ); + + if (await fse.pathExists(workDir)) { + const entries = await fsPromises.readdir(workDir); + if (entries.length > 0) { + const tmpArchiveFile = join(tmpdir(), getTmpArchiveFilename(taskId)); + try { + await tar.c( + { + gzip: true, + file: tmpArchiveFile, + cwd: workDir, + portable: true, + }, + entries, + ); + + if (!(await fse.pathExists(tmpArchiveFile))) { + throw new Error( + `tar.c command failed to create ${tmpArchiveFile}`, + ); + } + + const workspaceFile = this.storage + .bucket(this.bucketName) + .file(workspaceObjectPath); + const sourceStream = createReadStream(tmpArchiveFile); + const destStream = workspaceFile.createWriteStream({ + contentType: 'application/gzip', + resumable: true, + }); + + await new Promise((resolve, reject) => { + sourceStream.on('error', (err) => { + logger.error( + `Error in source stream for ${tmpArchiveFile}:`, + err, + ); + // Attempt to close destStream if source fails + if (!destStream.destroyed) { + destStream.destroy(err); + } + reject(err); + }); + + destStream.on('error', (err) => { + logger.error( + `Error in GCS dest stream for ${workspaceObjectPath}:`, + err, + ); + reject(err); + }); + + destStream.on('finish', () => { + logger.info( + `GCS destStream finished for ${workspaceObjectPath}`, + ); + resolve(); + }); + + logger.info( + `Piping ${tmpArchiveFile} to GCS object ${workspaceObjectPath}`, + ); + sourceStream.pipe(destStream); + }); + logger.info( + `Task ${taskId} workspace saved to GCS: gs://${this.bucketName}/${workspaceObjectPath}`, + ); + } catch (error) { + logger.error( + `Error during workspace save process for ${taskId}:`, + error, + ); + throw error; + } finally { + logger.info(`Cleaning up temporary file: ${tmpArchiveFile}`); + try { + if (await fse.pathExists(tmpArchiveFile)) { + await fse.remove(tmpArchiveFile); + logger.info( + `Successfully removed temporary file: ${tmpArchiveFile}`, + ); + } else { + logger.warn( + `Temporary file not found for cleanup: ${tmpArchiveFile}`, + ); + } + } catch (removeError) { + logger.error( + `Error removing temporary file ${tmpArchiveFile}:`, + removeError, + ); + } + } + } else { + logger.info( + `Workspace directory ${workDir} is empty, skipping workspace save for task ${taskId}.`, + ); + } + } else { + logger.info( + `Workspace directory ${workDir} not found, skipping workspace save for task ${taskId}.`, + ); + } + } catch (error) { + logger.error(`Failed to save task ${taskId} to GCS:`, error); + throw error; + } + } + + async load(taskId: string): Promise { + await this.ensureBucketInitialized(); + const metadataObjectPath = this.getObjectPath(taskId, 'metadata'); + const workspaceObjectPath = this.getObjectPath(taskId, 'workspace'); + + try { + const metadataFile = this.storage + .bucket(this.bucketName) + .file(metadataObjectPath); + const [metadataExists] = await metadataFile.exists(); + if (!metadataExists) { + logger.info(`Task ${taskId} metadata not found in GCS.`); + return undefined; + } + const [compressedMetadata] = await metadataFile.download(); + const jsonData = gunzipSync(compressedMetadata).toString(); + const loadedMetadata = JSON.parse(jsonData); + logger.info(`Task ${taskId} metadata loaded from GCS.`); + + const persistedState = getPersistedState(loadedMetadata); + if (!persistedState) { + throw new Error( + `Loaded metadata for task ${taskId} is missing internal persisted state.`, + ); + } + const agentSettings = persistedState._agentSettings; + + const workDir = setTargetDir(agentSettings); + await fse.ensureDir(workDir); + const workspaceFile = this.storage + .bucket(this.bucketName) + .file(workspaceObjectPath); + const [workspaceExists] = await workspaceFile.exists(); + if (workspaceExists) { + const tmpArchiveFile = join(tmpdir(), getTmpArchiveFilename(taskId)); + try { + await workspaceFile.download({ destination: tmpArchiveFile }); + await tar.x({ file: tmpArchiveFile, cwd: workDir }); + logger.info( + `Task ${taskId} workspace restored from GCS to ${workDir}`, + ); + } finally { + if (await fse.pathExists(tmpArchiveFile)) { + await fse.remove(tmpArchiveFile); + } + } + } else { + logger.info(`Task ${taskId} workspace archive not found in GCS.`); + } + + return { + id: taskId, + contextId: loadedMetadata._contextId || uuidv4(), + kind: 'task', + status: { + state: persistedState._taskState, + timestamp: new Date().toISOString(), + }, + metadata: loadedMetadata, + history: [], + artifacts: [], + }; + } catch (error) { + logger.error(`Failed to load task ${taskId} from GCS:`, error); + throw error; + } + } +} + +export class NoOpTaskStore implements TaskStore { + constructor(private realStore: TaskStore) {} + + async save(task: SDKTask): Promise { + logger.info(`[NoOpTaskStore] save called for task ${task.id} - IGNORED`); + return Promise.resolve(); + } + + async load(taskId: string): Promise { + logger.info( + `[NoOpTaskStore] load called for task ${taskId}, delegating to real store.`, + ); + return this.realStore.load(taskId); + } +} diff --git a/packages/a2a-server/src/index.ts b/packages/a2a-server/src/index.ts new file mode 100644 index 0000000000..2d0221fe82 --- /dev/null +++ b/packages/a2a-server/src/index.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './agent.js'; +export * from './types.js'; diff --git a/packages/a2a-server/src/logger.ts b/packages/a2a-server/src/logger.ts new file mode 100644 index 0000000000..8dca944b91 --- /dev/null +++ b/packages/a2a-server/src/logger.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import winston from 'winston'; + +const logger = winston.createLogger({ + level: 'info', + format: winston.format.combine( + // First, add a timestamp to the log info object + winston.format.timestamp({ + format: 'YYYY-MM-DD HH:mm:ss.SSS A', // Custom timestamp format + }), + // Here we define the custom output format + winston.format.printf((info) => { + const { level, timestamp, message, ...rest } = info; + return ( + `[${level.toUpperCase()}] ${timestamp} -- ${message}` + + `${Object.keys(rest).length > 0 ? `\n${JSON.stringify(rest, null, 2)}` : ''}` + ); // Only print ...rest if present + }), + ), + transports: [new winston.transports.Console()], +}); + +export { logger }; diff --git a/packages/a2a-server/src/metadata_types.ts b/packages/a2a-server/src/metadata_types.ts new file mode 100644 index 0000000000..4e3383826f --- /dev/null +++ b/packages/a2a-server/src/metadata_types.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { AgentSettings } from './types.js'; +import type { TaskState } from '@a2a-js/sdk'; + +export interface PersistedStateMetadata { + _agentSettings: AgentSettings; + _taskState: TaskState; +} + +export type PersistedTaskMetadata = { [k: string]: unknown }; + +export const METADATA_KEY = '__persistedState'; + +export function getPersistedState( + metadata: PersistedTaskMetadata, +): PersistedStateMetadata | undefined { + return metadata?.[METADATA_KEY] as PersistedStateMetadata | undefined; +} + +export function setPersistedState( + metadata: PersistedTaskMetadata, + state: PersistedStateMetadata, +): PersistedTaskMetadata { + return { + ...metadata, + [METADATA_KEY]: state, + }; +} diff --git a/packages/a2a-server/src/server.ts b/packages/a2a-server/src/server.ts new file mode 100644 index 0000000000..34a406ebad --- /dev/null +++ b/packages/a2a-server/src/server.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as url from 'node:url'; +import * as path from 'node:path'; + +import { logger } from './logger.js'; +import { main } from './agent.js'; + +// Check if the module is the main script being run. path.resolve() creates a +// canonical, absolute path, which avoids cross-platform issues. +const isMainModule = + path.resolve(process.argv[1]) === + path.resolve(url.fileURLToPath(import.meta.url)); + +process.on('uncaughtException', (error) => { + logger.error('Unhandled exception:', error); + process.exit(1); +}); + +if ( + import.meta.url.startsWith('file:') && + isMainModule && + process.env['NODE_ENV'] !== 'test' +) { + main().catch((error) => { + logger.error('[CoreAgent] Unhandled error in main:', error); + process.exit(1); + }); +} diff --git a/packages/a2a-server/src/settings.ts b/packages/a2a-server/src/settings.ts new file mode 100644 index 0000000000..dbe5129955 --- /dev/null +++ b/packages/a2a-server/src/settings.ts @@ -0,0 +1,154 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { homedir } from 'node:os'; + +import type { MCPServerConfig } from '@google/gemini-cli-core'; +import { + getErrorMessage, + type TelemetrySettings, +} from '@google/gemini-cli-core'; +import stripJsonComments from 'strip-json-comments'; + +export const SETTINGS_DIRECTORY_NAME = '.gemini'; +export const USER_SETTINGS_DIR = path.join(homedir(), SETTINGS_DIRECTORY_NAME); +export const USER_SETTINGS_PATH = path.join(USER_SETTINGS_DIR, 'settings.json'); + +// Reconcile with https://github.com/google-gemini/gemini-cli/blob/b09bc6656080d4d12e1d06734aae2ec33af5c1ed/packages/cli/src/config/settings.ts#L53 +export interface Settings { + mcpServers?: Record; + coreTools?: string[]; + excludeTools?: string[]; + telemetry?: TelemetrySettings; + showMemoryUsage?: boolean; + checkpointing?: CheckpointingSettings; + + // Git-aware file filtering settings + fileFiltering?: { + respectGitIgnore?: boolean; + enableRecursiveFileSearch?: boolean; + }; +} + +export interface SettingsError { + message: string; + path: string; +} + +export interface CheckpointingSettings { + enabled?: boolean; +} + +/** + * Loads settings from user and workspace directories. + * Project settings override user settings. + * + * How is it different to gemini-cli/cli: Returns already merged settings rather + * than `LoadedSettings` (unnecessary since we are not modifying users + * settings.json). + */ +export function loadSettings(workspaceDir: string): Settings { + let userSettings: Settings = {}; + let workspaceSettings: Settings = {}; + const settingsErrors: SettingsError[] = []; + + // Load user settings + try { + if (fs.existsSync(USER_SETTINGS_PATH)) { + const userContent = fs.readFileSync(USER_SETTINGS_PATH, 'utf-8'); + const parsedUserSettings = JSON.parse( + stripJsonComments(userContent), + ) as Settings; + userSettings = resolveEnvVarsInObject(parsedUserSettings); + } + } catch (error: unknown) { + settingsErrors.push({ + message: getErrorMessage(error), + path: USER_SETTINGS_PATH, + }); + } + + const workspaceSettingsPath = path.join( + workspaceDir, + SETTINGS_DIRECTORY_NAME, + 'settings.json', + ); + + // Load workspace settings + try { + if (fs.existsSync(workspaceSettingsPath)) { + const projectContent = fs.readFileSync(workspaceSettingsPath, 'utf-8'); + const parsedWorkspaceSettings = JSON.parse( + stripJsonComments(projectContent), + ) as Settings; + workspaceSettings = resolveEnvVarsInObject(parsedWorkspaceSettings); + } + } catch (error: unknown) { + settingsErrors.push({ + message: getErrorMessage(error), + path: workspaceSettingsPath, + }); + } + + if (settingsErrors.length > 0) { + console.error('Errors loading settings:'); + for (const error of settingsErrors) { + console.error(` Path: ${error.path}`); + console.error(` Message: ${error.message}`); + } + } + + // If there are overlapping keys, the values of workspaceSettings will + // override values from userSettings + return { + ...userSettings, + ...workspaceSettings, + }; +} + +function resolveEnvVarsInString(value: string): string { + const envVarRegex = /\$(?:(\w+)|{([^}]+)})/g; // Find $VAR_NAME or ${VAR_NAME} + return value.replace(envVarRegex, (match, varName1, varName2) => { + const varName = varName1 || varName2; + if (process && process.env && typeof process.env[varName] === 'string') { + return process.env[varName]!; + } + return match; + }); +} + +function resolveEnvVarsInObject(obj: T): T { + if ( + obj === null || + obj === undefined || + typeof obj === 'boolean' || + typeof obj === 'number' + ) { + return obj; + } + + if (typeof obj === 'string') { + return resolveEnvVarsInString(obj) as unknown as T; + } + + if (Array.isArray(obj)) { + return obj.map((item) => resolveEnvVarsInObject(item)) as unknown as T; + } + + if (typeof obj === 'object') { + const newObj = { ...obj } as T; + for (const key in newObj) { + if (Object.prototype.hasOwnProperty.call(newObj, key)) { + newObj[key] = resolveEnvVarsInObject(newObj[key]); + } + } + return newObj; + } + + return obj; +} diff --git a/packages/a2a-server/src/task.ts b/packages/a2a-server/src/task.ts new file mode 100644 index 0000000000..dfcedb46c6 --- /dev/null +++ b/packages/a2a-server/src/task.ts @@ -0,0 +1,930 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + CoreToolScheduler, + GeminiClient, + GeminiEventType, + ToolConfirmationOutcome, + ApprovalMode, + getAllMCPServerStatuses, + MCPServerStatus, + isNodeError, + parseAndFormatApiError, +} from '@google/gemini-cli-core'; +import type { + ToolConfirmationPayload, + CompletedToolCall, + ToolCall, + ToolCallRequestInfo, + ServerGeminiErrorEvent, + ServerGeminiStreamEvent, + ToolCallConfirmationDetails, + Config, + UserTierId, +} from '@google/gemini-cli-core'; +import type { RequestContext } from '@a2a-js/sdk/server'; +import { type ExecutionEventBus } from '@a2a-js/sdk/server'; +import type { + TaskStatusUpdateEvent, + TaskArtifactUpdateEvent, + TaskState, + Message, + Part, + Artifact, +} from '@a2a-js/sdk'; +import { v4 as uuidv4 } from 'uuid'; +import { logger } from './logger.js'; +import * as fs from 'node:fs'; + +import { CoderAgentEvent } from './types.js'; +import type { + CoderAgentMessage, + StateChange, + ToolCallUpdate, + TextContent, + TaskMetadata, + Thought, + ThoughtSummary, +} from './types.js'; +import type { PartUnion, Part as genAiPart } from '@google/genai'; + +export class Task { + id: string; + contextId: string; + scheduler: CoreToolScheduler; + config: Config; + geminiClient: GeminiClient; + pendingToolConfirmationDetails: Map; + taskState: TaskState; + eventBus?: ExecutionEventBus; + completedToolCalls: CompletedToolCall[]; + skipFinalTrueAfterInlineEdit = false; + + // For tool waiting logic + private pendingToolCalls: Map = new Map(); //toolCallId --> status + private toolCompletionPromise?: Promise; + private toolCompletionNotifier?: { + resolve: () => void; + reject: (reason?: Error) => void; + }; + + private constructor( + id: string, + contextId: string, + config: Config, + eventBus?: ExecutionEventBus, + ) { + this.id = id; + this.contextId = contextId; + this.config = config; + this.scheduler = this.createScheduler(); + this.geminiClient = new GeminiClient(this.config); + this.pendingToolConfirmationDetails = new Map(); + this.taskState = 'submitted'; + this.eventBus = eventBus; + this.completedToolCalls = []; + this._resetToolCompletionPromise(); + this.config.setFlashFallbackHandler( + async (currentModel: string, fallbackModel: string): Promise => { + config.setModel(fallbackModel); // gemini-cli-core sets to DEFAULT_GEMINI_FLASH_MODEL + // Switch model for future use but return false to stop current retry + return false; + }, + ); + } + + static async create( + id: string, + contextId: string, + config: Config, + eventBus?: ExecutionEventBus, + ): Promise { + return new Task(id, contextId, config, eventBus); + } + + // Note: `getAllMCPServerStatuses` retrieves the status of all MCP servers for the entire + // process. This is not scoped to the individual task but reflects the global connection + // state managed within the @gemini-cli/core module. + async getMetadata(): Promise { + const toolRegistry = await this.config.getToolRegistry(); + const mcpServers = this.config.getMcpServers() || {}; + const serverStatuses = getAllMCPServerStatuses(); + const servers = Object.keys(mcpServers).map((serverName) => ({ + name: serverName, + status: serverStatuses.get(serverName) || MCPServerStatus.DISCONNECTED, + tools: toolRegistry.getToolsByServer(serverName).map((tool) => ({ + name: tool.name, + description: tool.description, + parameterSchema: tool.schema.parameters, + })), + })); + + const availableTools = toolRegistry.getAllTools().map((tool) => ({ + name: tool.name, + description: tool.description, + parameterSchema: tool.schema.parameters, + })); + + const metadata: TaskMetadata = { + id: this.id, + contextId: this.contextId, + taskState: this.taskState, + model: this.config.getContentGeneratorConfig().model, + mcpServers: servers, + availableTools, + }; + return metadata; + } + + private _resetToolCompletionPromise(): void { + this.toolCompletionPromise = new Promise((resolve, reject) => { + this.toolCompletionNotifier = { resolve, reject }; + }); + // If there are no pending calls when reset, resolve immediately. + if (this.pendingToolCalls.size === 0 && this.toolCompletionNotifier) { + this.toolCompletionNotifier.resolve(); + } + } + + private _registerToolCall(toolCallId: string, status: string): void { + const wasEmpty = this.pendingToolCalls.size === 0; + this.pendingToolCalls.set(toolCallId, status); + if (wasEmpty) { + this._resetToolCompletionPromise(); + } + logger.info( + `[Task] Registered tool call: ${toolCallId}. Pending: ${this.pendingToolCalls.size}`, + ); + } + + private _resolveToolCall(toolCallId: string): void { + if (this.pendingToolCalls.has(toolCallId)) { + this.pendingToolCalls.delete(toolCallId); + logger.info( + `[Task] Resolved tool call: ${toolCallId}. Pending: ${this.pendingToolCalls.size}`, + ); + if (this.pendingToolCalls.size === 0 && this.toolCompletionNotifier) { + this.toolCompletionNotifier.resolve(); + } + } + } + + async waitForPendingTools(): Promise { + if (this.pendingToolCalls.size === 0) { + return Promise.resolve(); + } + logger.info( + `[Task] Waiting for ${this.pendingToolCalls.size} pending tool(s)...`, + ); + return this.toolCompletionPromise; + } + + cancelPendingTools(reason: string): void { + if (this.pendingToolCalls.size > 0) { + logger.info( + `[Task] Cancelling all ${this.pendingToolCalls.size} pending tool calls. Reason: ${reason}`, + ); + } + if (this.toolCompletionNotifier) { + this.toolCompletionNotifier.reject(new Error(reason)); + } + this.pendingToolCalls.clear(); + // Reset the promise for any future operations, ensuring it's in a clean state. + this._resetToolCompletionPromise(); + } + + private _createTextMessage( + text: string, + role: 'agent' | 'user' = 'agent', + ): Message { + return { + kind: 'message', + role, + parts: [{ kind: 'text', text }], + messageId: uuidv4(), + taskId: this.id, + contextId: this.contextId, + }; + } + + private _createStatusUpdateEvent( + stateToReport: TaskState, + coderAgentMessage: CoderAgentMessage, + message?: Message, + final = false, + timestamp?: string, + metadataError?: string, + ): TaskStatusUpdateEvent { + const metadata: { + coderAgent: CoderAgentMessage; + model: string; + userTier?: UserTierId; + error?: string; + } = { + coderAgent: coderAgentMessage, + model: this.config.getModel(), + userTier: this.geminiClient.getUserTier(), + }; + + if (metadataError) { + metadata.error = metadataError; + } + + return { + kind: 'status-update', + taskId: this.id, + contextId: this.contextId, + status: { + state: stateToReport, + message, // Shorthand property + timestamp: timestamp || new Date().toISOString(), + }, + final, + metadata, + }; + } + + setTaskStateAndPublishUpdate( + newState: TaskState, + coderAgentMessage: CoderAgentMessage, + messageText?: string, + messageParts?: Part[], // For more complex messages + final = false, + metadataError?: string, + ): void { + this.taskState = newState; + let message: Message | undefined; + + if (messageText) { + message = this._createTextMessage(messageText); + } else if (messageParts) { + message = { + kind: 'message', + role: 'agent', + parts: messageParts, + messageId: uuidv4(), + taskId: this.id, + contextId: this.contextId, + }; + } + + const event = this._createStatusUpdateEvent( + this.taskState, + coderAgentMessage, + message, + final, + undefined, + metadataError, + ); + this.eventBus?.publish(event); + } + + private _schedulerOutputUpdate( + toolCallId: string, + outputChunk: string, + ): void { + logger.info( + '[Task] Scheduler output update for tool call ' + + toolCallId + + ': ' + + outputChunk, + ); + const artifact: Artifact = { + artifactId: `tool-${toolCallId}-output`, + parts: [ + { + kind: 'text', + text: outputChunk, + } as Part, + ], + }; + const artifactEvent: TaskArtifactUpdateEvent = { + kind: 'artifact-update', + taskId: this.id, + contextId: this.contextId, + artifact, + append: true, + lastChunk: false, + }; + this.eventBus?.publish(artifactEvent); + } + + private async _schedulerAllToolCallsComplete( + completedToolCalls: CompletedToolCall[], + ): Promise { + logger.info( + '[Task] All tool calls completed by scheduler (batch):', + completedToolCalls.map((tc) => tc.request.callId), + ); + this.completedToolCalls.push(...completedToolCalls); + completedToolCalls.forEach((tc) => { + this._resolveToolCall(tc.request.callId); + }); + } + + private _schedulerToolCallsUpdate(toolCalls: ToolCall[]): void { + logger.info( + '[Task] Scheduler tool calls updated:', + toolCalls.map((tc) => `${tc.request.callId} (${tc.status})`), + ); + + // Update state and send continuous, non-final updates + toolCalls.forEach((tc) => { + const previousStatus = this.pendingToolCalls.get(tc.request.callId); + const hasChanged = previousStatus !== tc.status; + + // Resolve tool call if it has reached a terminal state + if (['success', 'error', 'cancelled'].includes(tc.status)) { + this._resolveToolCall(tc.request.callId); + } else { + // This will update the map + this._registerToolCall(tc.request.callId, tc.status); + } + + if (tc.status === 'awaiting_approval' && tc.confirmationDetails) { + this.pendingToolConfirmationDetails.set( + tc.request.callId, + tc.confirmationDetails, + ); + } + + // Only send an update if the status has actually changed. + if (hasChanged) { + const message = this.toolStatusMessage(tc, this.id, this.contextId); + const coderAgentMessage: CoderAgentMessage = + tc.status === 'awaiting_approval' + ? { kind: CoderAgentEvent.ToolCallConfirmationEvent } + : { kind: CoderAgentEvent.ToolCallUpdateEvent }; + + const event = this._createStatusUpdateEvent( + this.taskState, + coderAgentMessage, + message, + false, // Always false for these continuous updates + ); + this.eventBus?.publish(event); + } + }); + + if (this.config.getApprovalMode() === ApprovalMode.YOLO) { + logger.info('[Task] YOLO mode enabled. Auto-approving all tool calls.'); + toolCalls.forEach((tc: ToolCall) => { + if (tc.status === 'awaiting_approval' && tc.confirmationDetails) { + tc.confirmationDetails.onConfirm(ToolConfirmationOutcome.ProceedOnce); + this.pendingToolConfirmationDetails.delete(tc.request.callId); + } + }); + return; + } + + const allPendingStatuses = Array.from(this.pendingToolCalls.values()); + const isAwaitingApproval = allPendingStatuses.some( + (status) => status === 'awaiting_approval', + ); + const allPendingAreStable = allPendingStatuses.every( + (status) => + status === 'awaiting_approval' || + status === 'success' || + status === 'error' || + status === 'cancelled', + ); + + // 1. Are any pending tool calls awaiting_approval + // 2. Are all pending tool calls in a stable state (i.e. not in validing or executing) + // 3. After an inline edit, the edited tool call will send awaiting_approval THEN scheduled. We wait for the next update in this case. + if ( + isAwaitingApproval && + allPendingAreStable && + !this.skipFinalTrueAfterInlineEdit + ) { + this.skipFinalTrueAfterInlineEdit = false; + + // We don't need to send another message, just a final status update. + this.setTaskStateAndPublishUpdate( + 'input-required', + { kind: CoderAgentEvent.StateChangeEvent }, + undefined, + undefined, + /*final*/ true, + ); + } + } + + private createScheduler(): CoreToolScheduler { + const scheduler = new CoreToolScheduler({ + outputUpdateHandler: this._schedulerOutputUpdate.bind(this), + onAllToolCallsComplete: this._schedulerAllToolCallsComplete.bind(this), + onToolCallsUpdate: this._schedulerToolCallsUpdate.bind(this), + getPreferredEditor: () => 'vscode', + config: this.config, + onEditorClose: () => {}, + }); + return scheduler; + } + + private toolStatusMessage( + tc: ToolCall, + taskId: string, + contextId: string, + ): Message { + const messageParts: Part[] = []; + + // Create a serializable version of the ToolCall (pick necesssary + // properties/avoic methods causing circular reference errors) + const serializableToolCall: { [key: string]: unknown } = { + request: tc.request, + status: tc.status, + }; + + // For WaitingToolCall type + if ('confirmationDetails' in tc) { + serializableToolCall['confirmationDetails'] = tc.confirmationDetails; + } + + if (tc.tool) { + serializableToolCall['tool'] = { + name: tc.tool.name, + displayName: tc.tool.displayName, + description: tc.tool.description, + kind: tc.tool.kind, + isOutputMarkdown: tc.tool.isOutputMarkdown, + canUpdateOutput: tc.tool.canUpdateOutput, + schema: tc.tool.schema, + parameterSchema: tc.tool.parameterSchema, + }; + } + + messageParts.push({ + kind: 'data', + data: serializableToolCall as ToolCall, + } as Part); + + return { + kind: 'message', + role: 'agent', + parts: messageParts, + messageId: uuidv4(), + taskId, + contextId, + }; + } + + private async getProposedContent( + file_path: string, + old_string: string, + new_string: string, + ): Promise { + try { + const currentContent = fs.readFileSync(file_path, 'utf8'); + return this._applyReplacement( + currentContent, + old_string, + new_string, + old_string === '' && currentContent === '', + ); + } catch (err) { + if (!isNodeError(err) || err.code !== 'ENOENT') throw err; + return ''; + } + } + + private _applyReplacement( + currentContent: string | null, + oldString: string, + newString: string, + isNewFile: boolean, + ): string { + if (isNewFile) { + return newString; + } + if (currentContent === null) { + // Should not happen if not a new file, but defensively return empty or newString if oldString is also empty + return oldString === '' ? newString : ''; + } + // If oldString is empty and it's not a new file, do not modify the content. + if (oldString === '' && !isNewFile) { + return currentContent; + } + return currentContent.replaceAll(oldString, newString); + } + + async scheduleToolCalls( + requests: ToolCallRequestInfo[], + abortSignal: AbortSignal, + ): Promise { + if (requests.length === 0) { + return; + } + + for (const request of requests) { + if ( + !request.args['newContent'] && + request.name === 'replace' && + request.args && + request.args['file_path'] && + request.args['old_string'] && + request.args['new_string'] + ) { + request.args['newContent'] = await this.getProposedContent( + request.args['file_path'] as string, + request.args['old_string'] as string, + request.args['new_string'] as string, + ); + } + } + + logger.info(`[Task] Scheduling batch of ${requests.length} tool calls.`); + const stateChange: StateChange = { + kind: CoderAgentEvent.StateChangeEvent, + }; + this.setTaskStateAndPublishUpdate('working', stateChange); + + await this.scheduler.schedule(requests, abortSignal); + } + + async acceptAgentMessage(event: ServerGeminiStreamEvent): Promise { + const stateChange: StateChange = { + kind: CoderAgentEvent.StateChangeEvent, + }; + switch (event.type) { + case GeminiEventType.Content: + logger.info('[Task] Sending agent message content...'); + this._sendTextContent(event.value); + break; + case GeminiEventType.ToolCallRequest: + // This is now handled by the agent loop, which collects all requests + // and calls scheduleToolCalls once. + logger.warn( + '[Task] A single tool call request was passed to acceptAgentMessage. This should be handled in a batch by the agent. Ignoring.', + ); + break; + case GeminiEventType.ToolCallResponse: + // This event type from ServerGeminiStreamEvent might be for when LLM *generates* a tool response part. + // The actual execution result comes via user message. + logger.info( + '[Task] Received tool call response from LLM (part of generation):', + event.value, + ); + break; + case GeminiEventType.ToolCallConfirmation: + // This is when LLM requests confirmation, not when user provides it. + logger.info( + '[Task] Received tool call confirmation request from LLM:', + event.value.request.callId, + ); + this.pendingToolConfirmationDetails.set( + event.value.request.callId, + event.value.details, + ); + // This will be handled by the scheduler and _schedulerToolCallsUpdate will set InputRequired if needed. + // No direct state change here, scheduler drives it. + break; + case GeminiEventType.UserCancelled: + logger.info('[Task] Received user cancelled event from LLM stream.'); + this.cancelPendingTools('User cancelled via LLM stream event'); + this.setTaskStateAndPublishUpdate( + 'input-required', + stateChange, + 'Task cancelled by user', + undefined, + true, + ); + break; + case GeminiEventType.Thought: + logger.info('[Task] Sending agent thought...'); + this._sendThought(event.value); + break; + case GeminiEventType.ChatCompressed: + break; + case GeminiEventType.Finished: + logger.info(`[Task ${this.id}] Agent finished its turn.`); + break; + case GeminiEventType.Error: + default: { + // Block scope for lexical declaration + const errorEvent = event as ServerGeminiErrorEvent; // Type assertion + const errorMessage = + errorEvent.value?.error.message ?? 'Unknown error from LLM stream'; + logger.error( + '[Task] Received error event from LLM stream:', + errorMessage, + ); + + let errMessage = 'Unknown error from LLM stream'; + if (errorEvent.value) { + errMessage = parseAndFormatApiError(errorEvent.value); + } + this.cancelPendingTools(`LLM stream error: ${errorMessage}`); + this.setTaskStateAndPublishUpdate( + this.taskState, + stateChange, + `Agent Error, unknown agent message: ${errorMessage}`, + undefined, + false, + errMessage, + ); + break; + } + } + } + + private async _handleToolConfirmationPart(part: Part): Promise { + if ( + part.kind !== 'data' || + !part.data || + typeof part.data['callId'] !== 'string' || + typeof part.data['outcome'] !== 'string' + ) { + return false; + } + + const callId = part.data['callId'] as string; + const outcomeString = part.data['outcome'] as string; + let confirmationOutcome: ToolConfirmationOutcome | undefined; + + if (outcomeString === 'proceed_once') { + confirmationOutcome = ToolConfirmationOutcome.ProceedOnce; + } else if (outcomeString === 'cancel') { + confirmationOutcome = ToolConfirmationOutcome.Cancel; + } else if (outcomeString === 'proceed_always') { + confirmationOutcome = ToolConfirmationOutcome.ProceedAlways; + } else if (outcomeString === 'proceed_always_server') { + confirmationOutcome = ToolConfirmationOutcome.ProceedAlwaysServer; + } else if (outcomeString === 'proceed_always_tool') { + confirmationOutcome = ToolConfirmationOutcome.ProceedAlwaysTool; + } else if (outcomeString === 'modify_with_editor') { + confirmationOutcome = ToolConfirmationOutcome.ModifyWithEditor; + } else { + logger.warn( + `[Task] Unknown tool confirmation outcome: "${outcomeString}" for callId: ${callId}`, + ); + return false; + } + + const confirmationDetails = this.pendingToolConfirmationDetails.get(callId); + + if (!confirmationDetails) { + logger.warn( + `[Task] Received tool confirmation for unknown or already processed callId: ${callId}`, + ); + return false; + } + + logger.info( + `[Task] Handling tool confirmation for callId: ${callId} with outcome: ${outcomeString}`, + ); + try { + // Temporarily unset GCP environment variables so they do not leak into + // tool calls. + const gcpProject = process.env['GOOGLE_CLOUD_PROJECT']; + const gcpCreds = process.env['GOOGLE_APPLICATION_CREDENTIALS']; + try { + delete process.env['GOOGLE_CLOUD_PROJECT']; + delete process.env['GOOGLE_APPLICATION_CREDENTIALS']; + + // This will trigger the scheduler to continue or cancel the specific tool. + // The scheduler's onToolCallsUpdate will then reflect the new state (e.g., executing or cancelled). + + // If `edit` tool call, pass updated payload if presesent + if (confirmationDetails.type === 'edit') { + const payload = part.data['newContent'] + ? ({ + newContent: part.data['newContent'] as string, + } as ToolConfirmationPayload) + : undefined; + this.skipFinalTrueAfterInlineEdit = !!payload; + await confirmationDetails.onConfirm(confirmationOutcome, payload); + } else { + await confirmationDetails.onConfirm(confirmationOutcome); + } + } finally { + if (gcpProject) { + process.env['GOOGLE_CLOUD_PROJECT'] = gcpProject; + } + if (gcpCreds) { + process.env['GOOGLE_APPLICATION_CREDENTIALS'] = gcpCreds; + } + } + + // Do not delete if modifying, a subsequent tool confirmation for the same + // callId will be passed with ProceedOnce/Cancel/etc + // Note !== ToolConfirmationOutcome.ModifyWithEditor does not work! + if (confirmationOutcome !== 'modify_with_editor') { + this.pendingToolConfirmationDetails.delete(callId); + } + + // If outcome is Cancel, scheduler should update status to 'cancelled', which then resolves the tool. + // If ProceedOnce, scheduler updates to 'executing', then eventually 'success'/'error', which resolves. + return true; + } catch (error) { + logger.error( + `[Task] Error during tool confirmation for callId ${callId}:`, + error, + ); + // If confirming fails, we should probably mark this tool as failed + this._resolveToolCall(callId); // Resolve it as it won't proceed. + const errorMessageText = + error instanceof Error + ? error.message + : `Error processing tool confirmation for ${callId}`; + const message = this._createTextMessage(errorMessageText); + const toolCallUpdate: ToolCallUpdate = { + kind: CoderAgentEvent.ToolCallUpdateEvent, + }; + const event = this._createStatusUpdateEvent( + this.taskState, + toolCallUpdate, + message, + false, + ); + this.eventBus?.publish(event); + return false; + } + } + + getAndClearCompletedTools(): CompletedToolCall[] { + const tools = [...this.completedToolCalls]; + this.completedToolCalls = []; + return tools; + } + + addToolResponsesToHistory(completedTools: CompletedToolCall[]): void { + logger.info( + `[Task] Adding ${completedTools.length} tool responses to history without generating a new response.`, + ); + const responsesToAdd = completedTools.flatMap( + (toolCall) => toolCall.response.responseParts, + ); + + for (const response of responsesToAdd) { + let parts: genAiPart[]; + if (Array.isArray(response)) { + parts = response; + } else if (typeof response === 'string') { + parts = [{ text: response }]; + } else { + parts = [response]; + } + this.geminiClient.addHistory({ + role: 'user', + parts, + }); + } + } + + async *sendCompletedToolsToLlm( + completedToolCalls: CompletedToolCall[], + aborted: AbortSignal, + ): AsyncGenerator { + if (completedToolCalls.length === 0) { + yield* (async function* () {})(); // Yield nothing + return; + } + + const llmParts: PartUnion[] = []; + logger.info( + `[Task] Feeding ${completedToolCalls.length} tool responses to LLM.`, + ); + for (const completedToolCall of completedToolCalls) { + logger.info( + `[Task] Adding tool response for "${completedToolCall.request.name}" (callId: ${completedToolCall.request.callId}) to LLM input.`, + ); + const responseParts = completedToolCall.response.responseParts; + if (Array.isArray(responseParts)) { + llmParts.push(...responseParts); + } else { + llmParts.push(responseParts); + } + } + + logger.info('[Task] Sending new parts to agent.'); + const stateChange: StateChange = { + kind: CoderAgentEvent.StateChangeEvent, + }; + // Set task state to working as we are about to call LLM + this.setTaskStateAndPublishUpdate('working', stateChange); + // TODO: Determine what it mean to have, then add a prompt ID. + yield* this.geminiClient.sendMessageStream( + llmParts, + aborted, + /*prompt_id*/ '', + ); + } + + async *acceptUserMessage( + requestContext: RequestContext, + aborted: AbortSignal, + ): AsyncGenerator { + const userMessage = requestContext.userMessage; + const llmParts: PartUnion[] = []; + let anyConfirmationHandled = false; + let hasContentForLlm = false; + + for (const part of userMessage.parts) { + const confirmationHandled = await this._handleToolConfirmationPart(part); + if (confirmationHandled) { + anyConfirmationHandled = true; + // If a confirmation was handled, the scheduler will now run the tool (or cancel it). + // We don't send anything to the LLM for this part. + // The subsequent tool execution will eventually lead to resolveToolCall. + continue; + } + + if (part.kind === 'text') { + llmParts.push({ text: part.text }); + hasContentForLlm = true; + } + } + + if (hasContentForLlm) { + logger.info('[Task] Sending new parts to LLM.'); + const stateChange: StateChange = { + kind: CoderAgentEvent.StateChangeEvent, + }; + // Set task state to working as we are about to call LLM + this.setTaskStateAndPublishUpdate('working', stateChange); + // TODO: Determine what it mean to have, then add a prompt ID. + yield* this.geminiClient.sendMessageStream( + llmParts, + aborted, + /*prompt_id*/ '', + ); + } else if (anyConfirmationHandled) { + logger.info( + '[Task] User message only contained tool confirmations. Scheduler is active. No new input for LLM this turn.', + ); + // Ensure task state reflects that scheduler might be working due to confirmation. + // If scheduler is active, it will emit its own status updates. + // If all pending tools were just confirmed, waitForPendingTools will handle the wait. + // If some tools are still pending approval, scheduler would have set InputRequired. + // If not, and no new text, we are just waiting. + if ( + this.pendingToolCalls.size > 0 && + this.taskState !== 'input-required' + ) { + const stateChange: StateChange = { + kind: CoderAgentEvent.StateChangeEvent, + }; + this.setTaskStateAndPublishUpdate('working', stateChange); // Reflect potential background activity + } + yield* (async function* () {})(); // Yield nothing + } else { + logger.info( + '[Task] No relevant parts in user message for LLM interaction or tool confirmation.', + ); + // If there's no new text and no confirmations, and no pending tools, + // it implies we might need to signal input required if nothing else is happening. + // However, the agent.ts will make this determination after waitForPendingTools. + yield* (async function* () {})(); // Yield nothing + } + } + + _sendTextContent(content: string): void { + if (content === '') { + return; + } + logger.info('[Task] Sending text content to event bus.'); + const message = this._createTextMessage(content); + const textContent: TextContent = { + kind: CoderAgentEvent.TextContentEvent, + }; + this.eventBus?.publish( + this._createStatusUpdateEvent( + this.taskState, + textContent, + message, + false, + ), + ); + } + + _sendThought(content: ThoughtSummary): void { + if (!content.subject && !content.description) { + return; + } + logger.info('[Task] Sending thought to event bus.'); + const message: Message = { + kind: 'message', + role: 'agent', + parts: [ + { + kind: 'data', + data: content, + } as Part, + ], + messageId: uuidv4(), + taskId: this.id, + contextId: this.contextId, + }; + const thought: Thought = { + kind: CoderAgentEvent.ThoughtEvent, + }; + this.eventBus?.publish( + this._createStatusUpdateEvent(this.taskState, thought, message, false), + ); + } +} diff --git a/packages/a2a-server/src/testing_utils.ts b/packages/a2a-server/src/testing_utils.ts new file mode 100644 index 0000000000..bd7ddaaa87 --- /dev/null +++ b/packages/a2a-server/src/testing_utils.ts @@ -0,0 +1,180 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { + Task as SDKTask, + TaskStatusUpdateEvent, + SendStreamingMessageSuccessResponse, +} from '@a2a-js/sdk'; +import { + BaseDeclarativeTool, + BaseToolInvocation, + Kind, +} from '@google/gemini-cli-core'; +import type { + ToolCallConfirmationDetails, + ToolResult, + ToolInvocation, +} from '@google/gemini-cli-core'; +import { expect, vi } from 'vitest'; + +export const mockOnUserConfirmForToolConfirmation = vi.fn(); + +export class MockToolInvocation extends BaseToolInvocation { + constructor( + private readonly tool: MockTool, + params: object, + ) { + super(params); + } + + getDescription(): string { + return JSON.stringify(this.params); + } + + override shouldConfirmExecute( + abortSignal: AbortSignal, + ): Promise { + return this.tool.shouldConfirmExecute(this.params, abortSignal); + } + + execute( + signal: AbortSignal, + updateOutput?: (output: string) => void, + terminalColumns?: number, + terminalRows?: number, + ): Promise { + return this.tool.execute( + this.params, + signal, + updateOutput, + terminalColumns, + terminalRows, + ); + } +} + +// TODO: dedup with gemini-cli, add shouldConfirmExecute() support in core +export class MockTool extends BaseDeclarativeTool { + constructor( + name: string, + displayName: string, + canUpdateOutput = false, + isOutputMarkdown = false, + shouldConfirmExecute?: () => Promise, + ) { + super( + name, + displayName, + 'A mock tool for testing', + Kind.Other, + {}, + isOutputMarkdown, + canUpdateOutput, + ); + + if (shouldConfirmExecute) { + this.shouldConfirmExecute.mockImplementation(shouldConfirmExecute); + } else { + // Default to no confirmation needed + this.shouldConfirmExecute.mockResolvedValue(false); + } + } + + execute = vi.fn(); + shouldConfirmExecute = vi.fn(); + + protected createInvocation( + params: object, + ): ToolInvocation { + return new MockToolInvocation(this, params); + } +} + +export function createStreamMessageRequest( + text: string, + messageId: string, + taskId?: string, +) { + const request: { + jsonrpc: string; + id: string; + method: string; + params: { + message: { + kind: string; + role: string; + parts: [{ kind: string; text: string }]; + messageId: string; + }; + metadata: { + coderAgent: { + kind: string; + workspacePath: string; + }; + }; + taskId?: string; + }; + } = { + jsonrpc: '2.0', + id: '1', + method: 'message/stream', + params: { + message: { + kind: 'message', + role: 'user', + parts: [{ kind: 'text', text }], + messageId, + }, + metadata: { + coderAgent: { + kind: 'agent-settings', + workspacePath: '/tmp', + }, + }, + }, + }; + + if (taskId) { + request.params.taskId = taskId; + } + + return request; +} + +export function assertUniqueFinalEventIsLast( + events: SendStreamingMessageSuccessResponse[], +) { + // Final event is input-required & final + const finalEvent = events[events.length - 1].result as TaskStatusUpdateEvent; + expect(finalEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'state-change', + }); + expect(finalEvent.status?.state).toBe('input-required'); + expect(finalEvent.final).toBe(true); + + // There is only one event with final and its the last + expect( + events.filter((e) => (e.result as TaskStatusUpdateEvent).final).length, + ).toBe(1); + expect( + events.findIndex((e) => (e.result as TaskStatusUpdateEvent).final), + ).toBe(events.length - 1); +} + +export function assertTaskCreationAndWorkingStatus( + events: SendStreamingMessageSuccessResponse[], +) { + // Initial task creation event + const taskEvent = events[0].result as SDKTask; + expect(taskEvent.kind).toBe('task'); + expect(taskEvent.status.state).toBe('submitted'); + + // Status update: working + const workingEvent = events[1].result as TaskStatusUpdateEvent; + expect(workingEvent.kind).toBe('status-update'); + expect(workingEvent.status.state).toBe('working'); +} diff --git a/packages/a2a-server/src/types.ts b/packages/a2a-server/src/types.ts new file mode 100644 index 0000000000..5a82059b47 --- /dev/null +++ b/packages/a2a-server/src/types.ts @@ -0,0 +1,104 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { + MCPServerStatus, + ToolConfirmationOutcome, +} from '@google/gemini-cli-core'; +import type { TaskState } from '@a2a-js/sdk'; + +// Interfaces and enums for the CoderAgent protocol. + +export enum CoderAgentEvent { + /** + * An event requesting one or more tool call confirmations. + */ + ToolCallConfirmationEvent = 'tool-call-confirmation', + /** + * An event updating on the status of one or more tool calls. + */ + ToolCallUpdateEvent = 'tool-call-update', + /** + * An event providing text updates on the task. + */ + TextContentEvent = 'text-content', + /** + * An event that indicates a change in the task's execution state. + */ + StateChangeEvent = 'state-change', + /** + * An user-sent event to initiate the agent. + */ + StateAgentSettingsEvent = 'agent-settings', + /** + * An event that contains a thought from the agent. + */ + ThoughtEvent = 'thought', +} + +export interface AgentSettings { + kind: CoderAgentEvent.StateAgentSettingsEvent; + workspacePath: string; +} + +export interface ToolCallConfirmation { + kind: CoderAgentEvent.ToolCallConfirmationEvent; +} + +export interface ToolCallUpdate { + kind: CoderAgentEvent.ToolCallUpdateEvent; +} + +export interface TextContent { + kind: CoderAgentEvent.TextContentEvent; +} + +export interface StateChange { + kind: CoderAgentEvent.StateChangeEvent; +} + +export interface Thought { + kind: CoderAgentEvent.ThoughtEvent; +} + +export type ThoughtSummary = { + subject: string; + description: string; +}; + +export interface ToolConfirmationResponse { + outcome: ToolConfirmationOutcome; + callId: string; +} + +export type CoderAgentMessage = + | AgentSettings + | ToolCallConfirmation + | ToolCallUpdate + | TextContent + | StateChange + | Thought; + +export interface TaskMetadata { + id: string; + contextId: string; + taskState: TaskState; + model: string; + mcpServers: Array<{ + name: string; + status: MCPServerStatus; + tools: Array<{ + name: string; + description: string; + parameterSchema: unknown; + }>; + }>; + availableTools: Array<{ + name: string; + description: string; + parameterSchema: unknown; + }>; +} diff --git a/packages/a2a-server/tsconfig.json b/packages/a2a-server/tsconfig.json new file mode 100644 index 0000000000..b788af471a --- /dev/null +++ b/packages/a2a-server/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "lib": ["DOM", "DOM.Iterable", "ES2021"], + "composite": true, + "types": ["node", "vitest/globals"] + }, + "include": ["index.ts", "src/**/*.ts", "src/**/*.json"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/a2a-server/vitest.config.ts b/packages/a2a-server/vitest.config.ts new file mode 100644 index 0000000000..68332c394a --- /dev/null +++ b/packages/a2a-server/vitest.config.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + reporters: [['default'], ['junit', { outputFile: 'junit.xml' }]], + passWithNoTests: true, + coverage: { + provider: 'v8', + reportsDirectory: './coverage', + reporter: [ + ['text', { file: 'full-text-summary.txt' }], + 'html', + 'json', + 'lcov', + 'cobertura', + ['json-summary', { outputFile: 'coverage-summary.json' }], + ], + }, + }, +}); diff --git a/packages/cli/package.json b/packages/cli/package.json index f9adeb7440..267198e7dd 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -57,6 +57,7 @@ }, "devDependencies": { "@babel/runtime": "^7.27.6", + "@google/gemini-cli-test-utils": "file:../test-utils", "@testing-library/react": "^16.3.0", "@types/command-exists": "^1.2.3", "@types/diff": "^7.0.2", @@ -72,8 +73,7 @@ "pretty-format": "^30.0.2", "react-dom": "^19.1.0", "typescript": "^5.3.3", - "vitest": "^3.1.1", - "@google/gemini-cli-test-utils": "file:../test-utils" + "vitest": "^3.1.1" }, "engines": { "node": ">=20" diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts index 0d2e4b07c4..7099e2e669 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts @@ -84,6 +84,7 @@ import { FileCommandLoader } from '../../services/FileCommandLoader.js'; import { McpPromptLoader } from '../../services/McpPromptLoader.js'; import { SlashCommandStatus, + ToolConfirmationOutcome, makeFakeConfig, ToolConfirmationOutcome, type IdeClient, diff --git a/packages/core/package.json b/packages/core/package.json index f2c0cec850..bcf49806fc 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -34,6 +34,7 @@ "@types/glob": "^8.1.0", "@types/html-to-text": "^9.0.4", "ajv": "^8.17.1", + "fast-uri": "^3.0.6", "ajv-formats": "^3.0.0", "chardet": "^2.1.0", "diff": "^7.0.0", diff --git a/packages/vscode-ide-companion/tsconfig.json b/packages/vscode-ide-companion/tsconfig.json index 2fec2bd935..f135706485 100644 --- a/packages/vscode-ide-companion/tsconfig.json +++ b/packages/vscode-ide-companion/tsconfig.json @@ -5,6 +5,13 @@ "target": "ES2022", "lib": ["ES2022", "dom"], "sourceMap": true, + /* + * skipLibCheck is necessary because the a2a-server package depends on + * @google-cloud/storage which pulls in @types/request which depends on + * tough-cookie@4.x while jsdom requires tough-cookie@5.x. This causes a + * type checking error in ../../node_modules/@types/request/index.d.ts. + */ + "skipLibCheck": true, "rootDir": "src", "strict": true /* enable all strict type-checking options */ /* Additional Checks */