diff --git a/Documentation.md b/Documentation.md index d57eff4..c795966 100644 --- a/Documentation.md +++ b/Documentation.md @@ -60,7 +60,7 @@ curl -fsSL https://raw.githubusercontent.com/open-gitagent/gitagent/main/install **Manual install:** ```bash -npm install -g @open-gitagent/gitagent +npm install -g @open-gitagent/gitagent @open-gitagent/voice # omit @open-gitagent/voice for slim CLI/SDK only mkdir ~/assistant && cd ~/assistant && git init gitagent --voice --dir . ``` diff --git a/README.md b/README.md index c684c6d..526cd8d 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,37 @@ This will: ### Or install manually: ```bash +# Slim CLI + SDK (recommended in sandboxed/CI environments where supply-chain +# scanners reject larger bundles) npm install -g @open-gitagent/gitagent + +# Add voice mode + web UI (the same web UI install.sh launches at :3333) +npm install -g @open-gitagent/voice ``` +`install.sh` installs both packages by default. Set `GITAGENT_SLIM=1` before +the curl-bash to skip voice. + +## Migrating from 1.x → 2.0 + +Voice mode lives in `@open-gitagent/voice` now. The reason: as a single bundle, +the package was being blocked by some supply-chain scanners that flagged its +3,800-line `dist/voice/ui.html` and the unused `baileys` dependency. Splitting +voice out drops the slim-core tarball from ~180 kB to ~85 kB and removes the +scanner triggers entirely. + +```bash +# If you were on v1.x and used voice: +npm install -g @open-gitagent/gitagent@latest @open-gitagent/voice + +# If you only use the SDK / non-voice CLI: +npm install -g @open-gitagent/gitagent@latest +``` + +The `gitagent` command and `@open-gitagent/gitagent` SDK exports are unchanged. +`gitagent --voice` dynamically loads `@open-gitagent/voice`; without it +installed, it prints a one-line install hint and exits cleanly. + ## Quick Start **Run your first agent in one line:** @@ -752,7 +780,7 @@ Your agent lives in a git repository with structured files: ### Installation & Setup **What are the requirements?** -Node.js 18+ (or 20+ recommended), npm, and git. Install globally with `npm install -g @open-gitagent/gitagent`. +Node.js 18+ (or 20+ recommended), npm, and git. Install globally with `npm install -g @open-gitagent/gitagent` (slim CLI + SDK). Add `@open-gitagent/voice` for voice mode + the web UI. **How do I set up API keys?** Run the installer for guided setup: diff --git a/install.sh b/install.sh index 1325f44..a2dd4ad 100755 --- a/install.sh +++ b/install.sh @@ -106,17 +106,28 @@ if [ "$(uname)" != "Darwin" ] && ! npm root -g 2>/dev/null | grep -q "$HOME"; th NPM_CMD="sudo npm" fi +# v2.0+ ships voice as a separate optional package. We install both by default +# so the curl-bash UX is unchanged. Set GITAGENT_SLIM=1 before the install +# (e.g. in sandboxed/CI environments where the voice package's larger surface +# could trip supply-chain scanners) to install the core only. +NPM_PACKAGES="@open-gitagent/gitagent@latest" +VOICE_LABEL="(core only — GITAGENT_SLIM=1)" +if [ "${GITAGENT_SLIM:-}" != "1" ]; then + NPM_PACKAGES="$NPM_PACKAGES @open-gitagent/voice@latest" + VOICE_LABEL="(core + voice)" +fi + if command -v gitagent &>/dev/null; then INSTALLED_VER="$(npm ls -g @open-gitagent/gitagent --depth=0 --json 2>/dev/null | node -pe "JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).dependencies?.['@open-gitagent/gitagent']?.version || ''" 2>/dev/null || echo "")" LATEST_VER="$(npm view @open-gitagent/gitagent version 2>/dev/null || echo "")" if [ -n "$INSTALLED_VER" ] && [ -n "$LATEST_VER" ] && [ "$INSTALLED_VER" != "$LATEST_VER" ]; then - echo -e " ${YELLOW}⬆${NC} gitagent ${DIM}v${INSTALLED_VER}${NC} installed — ${GREEN}v${LATEST_VER}${NC} available" + echo -e " ${YELLOW}⬆${NC} gitagent ${DIM}v${INSTALLED_VER}${NC} installed — ${GREEN}v${LATEST_VER}${NC} available ${DIM}${VOICE_LABEL}${NC}" read -rp " Update to v${LATEST_VER}? [Y/n]: " UPDATE_CHOICE UPDATE_CHOICE="${UPDATE_CHOICE:-Y}" if [[ "$UPDATE_CHOICE" =~ ^[Yy] ]]; then echo -e " ${BOLD}Updating gitagent...${NC}" - $NPM_CMD install -g @open-gitagent/gitagent@latest 2>&1 | tail -2 + $NPM_CMD install -g $NPM_PACKAGES 2>&1 | tail -2 echo -e " ${GREEN}✓${NC} gitagent updated to v${LATEST_VER}" else echo -e " ${DIM} keeping v${INSTALLED_VER}${NC}" @@ -125,13 +136,13 @@ if command -v gitagent &>/dev/null; then echo -e " ${GREEN}✓${NC} gitagent v${INSTALLED_VER:-latest} ${DIM}(up to date)${NC}" fi else - echo -e " ${BOLD}Installing gitagent...${NC}" + echo -e " ${BOLD}Installing gitagent...${NC} ${DIM}${VOICE_LABEL}${NC}" # Remove corrupted partial installs that cause ENOTDIR NPM_GLOBAL_DIR="$(npm root -g 2>/dev/null || echo "")" if [ -n "$NPM_GLOBAL_DIR" ] && [ -d "${NPM_GLOBAL_DIR}/@open-gitagent/gitagent" ] && [ ! -f "${NPM_GLOBAL_DIR}/@open-gitagent/gitagent/package.json" ]; then $NPM_CMD rm -rf "${NPM_GLOBAL_DIR}/@open-gitagent/gitagent" 2>/dev/null fi - $NPM_CMD install -g @open-gitagent/gitagent@latest 2>&1 | tail -2 + $NPM_CMD install -g $NPM_PACKAGES 2>&1 | tail -2 echo -e " ${GREEN}✓${NC} gitagent installed" fi echo "" diff --git a/package-lock.json b/package-lock.json index 1e81523..2316271 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,14 @@ { "name": "@open-gitagent/gitagent", - "version": "1.5.2", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@open-gitagent/gitagent", - "version": "1.5.2", + "version": "2.0.0", "license": "MIT", "dependencies": { - "@googleworkspace/cli": "^0.8.1", "@mariozechner/pi-agent-core": "^0.70.2", "@mariozechner/pi-ai": "^0.70.2", "@opentelemetry/api": "^1.9.0", @@ -23,10 +22,8 @@ "@opentelemetry/sdk-trace-node": "^2.7.0", "@opentelemetry/semantic-conventions": "^1.40.0", "@sinclair/typebox": "^0.34.41", - "baileys": "^7.0.0-rc.9", "js-yaml": "^4.1.0", "node-cron": "^3.0.3", - "ws": "^8.19.0", "yaml": "^2.8.2" }, "bin": { @@ -36,7 +33,6 @@ "@types/js-yaml": "^4.0.9", "@types/node": "^22.0.0", "@types/node-cron": "^3.0.11", - "@types/ws": "^8.18.1", "typescript": "^5.7.0" }, "engines": { @@ -135,24 +131,24 @@ } }, "node_modules/@aws-sdk/client-bedrock-runtime": { - "version": "3.1053.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1053.0.tgz", - "integrity": "sha512-I5dua8y1logE+Mx6r5kvI1tjM+XyC3H42KDCpEqmhrJfanor/x/AdOavyv3HnS4sBqUxx2IrjLP3ouEumjeTzA==", + "version": "3.1063.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1063.0.tgz", + "integrity": "sha512-zTWEIvCFDJ13VjAyK8UouphesohVgZr3u7r6f74w8rtypPkci2vZtmttKxj/eeX2GQipuyHNgPwut2eXoL28aA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/credential-provider-node": "^3.972.44", - "@aws-sdk/eventstream-handler-node": "^3.972.17", - "@aws-sdk/middleware-eventstream": "^3.972.13", - "@aws-sdk/middleware-websocket": "^3.972.21", - "@aws-sdk/token-providers": "3.1053.0", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/fetch-http-handler": "^5.4.3", - "@smithy/node-http-handler": "^4.7.3", - "@smithy/types": "^4.14.2", + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/credential-provider-node": "^3.972.52", + "@aws-sdk/eventstream-handler-node": "^3.972.20", + "@aws-sdk/middleware-eventstream": "^3.972.16", + "@aws-sdk/middleware-websocket": "^3.972.26", + "@aws-sdk/token-providers": "3.1063.0", + "@aws-sdk/types": "^3.973.11", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -160,17 +156,17 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.974.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.13.tgz", - "integrity": "sha512-+Y5/4tHki0uYgyx8eun146DegRVQBpdKGK5RbV0FTKJPpaKTchvqVxrrRFK6Wk0JksO4iAZKw3eqxGEIwtO98w==", + "version": "3.974.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.18.tgz", + "integrity": "sha512-JDYCPI0j7zGrzXTDFsLB346cxss7J/AxH7+O0MzWlqppJBEyB9Qe6TQXRL6iwLUo/xZkNv9KFmBL2hqElmwW0g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.9", - "@aws-sdk/xml-builder": "^3.972.25", + "@aws-sdk/types": "^3.973.11", + "@aws-sdk/xml-builder": "^3.972.28", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/core": "^3.24.3", - "@smithy/signature-v4": "^5.4.2", - "@smithy/types": "^4.14.2", + "@smithy/core": "^3.24.6", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", "bowser": "^2.11.0", "tslib": "^2.6.2" }, @@ -179,15 +175,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.39", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.39.tgz", - "integrity": "sha512-29wX9zpAvEt1vcj0psha+y6ygBHy2V/S72mp6e7q0KARLWXq+pwE/lR6qGkwknQvruh52lXvlqZIga8Hdxkucw==", + "version": "3.972.44", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.44.tgz", + "integrity": "sha512-3hKJVrZ7bqXzDAXCQp+OaQ1ASN+vWstaNuEH418wQVl//cRZhqhfR9Bjk1qIWmgUGe8/D3gdO73PgidRj378EQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/types": "^3.973.11", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -195,17 +191,17 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.41", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.41.tgz", - "integrity": "sha512-IA3CQTjtJkb6u1H4mE4936c8OPBMa9Jggtwe8U2Mqw/vvb/tZ5Ebd0mcZcX0uKWQhOyYo/+qNIwkV5Xh+FeJJA==", + "version": "3.972.46", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.46.tgz", + "integrity": "sha512-VhwC9pGAZHhiQ2xSViyOPDFqvr9aRxGCAXZtADsUhU3R65nad7y//CwynE6mQnWNR+suRlqE79W36IVayL+m1g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/fetch-http-handler": "^5.4.3", - "@smithy/node-http-handler": "^4.7.3", - "@smithy/types": "^4.14.2", + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/types": "^3.973.11", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -213,23 +209,23 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.43", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.43.tgz", - "integrity": "sha512-4mzII+3mZEVXXE1xzrLQrCJL7/r62A63bA6SVzZoNL5rqCJghpf+xgGltVrIBBs0n+mOZBKrQl2tRREtvZ5l6A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/credential-provider-env": "^3.972.39", - "@aws-sdk/credential-provider-http": "^3.972.41", - "@aws-sdk/credential-provider-login": "^3.972.43", - "@aws-sdk/credential-provider-process": "^3.972.39", - "@aws-sdk/credential-provider-sso": "^3.972.43", - "@aws-sdk/credential-provider-web-identity": "^3.972.43", - "@aws-sdk/nested-clients": "^3.997.11", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/credential-provider-imds": "^4.3.2", - "@smithy/types": "^4.14.2", + "version": "3.972.50", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.50.tgz", + "integrity": "sha512-09Xi6ovxiK42+De/qBGF71sT5F2bWgYM+1fFyDwSOpy1xpsQ5R/naIu7MVDpH6Dic36QNc8dAv4KADtMGK2JYg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/credential-provider-env": "^3.972.44", + "@aws-sdk/credential-provider-http": "^3.972.46", + "@aws-sdk/credential-provider-login": "^3.972.49", + "@aws-sdk/credential-provider-process": "^3.972.44", + "@aws-sdk/credential-provider-sso": "^3.972.49", + "@aws-sdk/credential-provider-web-identity": "^3.972.49", + "@aws-sdk/nested-clients": "^3.997.17", + "@aws-sdk/types": "^3.973.11", + "@smithy/core": "^3.24.6", + "@smithy/credential-provider-imds": "^4.3.7", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -237,1424 +233,306 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.43", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.43.tgz", - "integrity": "sha512-HG7kQCwXtbv3oBV61Ins0oNX8KKyvrMqqRkb6ZiAfQHbMuHaiNaEb2KnpKLPkNpqImSBK82UkVE/kaY6IfWikA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/nested-clients": "^3.997.11", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.44", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.44.tgz", - "integrity": "sha512-sDaBIT0yrNNIPfvlsiTCmANm07zKju+ipWODjEXgZlsjMeIJR3LVp7RDyAOzUoAsTbDfYKDWp+i5WrFiQP6rmQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.39", - "@aws-sdk/credential-provider-http": "^3.972.41", - "@aws-sdk/credential-provider-ini": "^3.972.43", - "@aws-sdk/credential-provider-process": "^3.972.39", - "@aws-sdk/credential-provider-sso": "^3.972.43", - "@aws-sdk/credential-provider-web-identity": "^3.972.43", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/credential-provider-imds": "^4.3.2", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.39", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.39.tgz", - "integrity": "sha512-2k/amBifLd75eXNwgvPw/2lKYSQ3NhvHQgkVKVjfUq13/eJ3JRtHmznuFenn74OK3sSfp4SMy1YB2w+UVXoKqA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.43", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.43.tgz", - "integrity": "sha512-LPc3+Y4vhH1T4x6CMqwCM6hk5+SRf/Lwmgm8INm95wxTtIRHcMwQUVkDzWu4Iw/RSncxYM2BC01OrYbxOPZvyg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/nested-clients": "^3.997.11", - "@aws-sdk/token-providers": "3.1052.0", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { - "version": "3.1052.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1052.0.tgz", - "integrity": "sha512-QqZNB3so7UIDxZtroc85TQaLVxdZRFm0eWM1CSR2N+b06as9TOrilvrlTZuj3guYlxMs6yLOgGxnklJ5qMYtTw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/nested-clients": "^3.997.11", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.43", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.43.tgz", - "integrity": "sha512-wQtL34lUD/09VXjwAUo2T+I3aEXRDxMB3DKmTJL/Zj0Gi6sLDTrVhae1XVt01yzkquOWajI/sZW72JGDZ1ciTw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/nested-clients": "^3.997.11", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/eventstream-handler-node": { - "version": "3.972.17", - "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.17.tgz", - "integrity": "sha512-WFwdNcjchKZr7jKYgGimUZO8sSKQF/le7GGqgeCzz/lHozInE6b0gFJ1YMr8NaIeAoWJwgtrF7RE4/qMgosAdQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-eventstream": { - "version": "3.972.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.13.tgz", - "integrity": "sha512-ECfsw7mf6G/sxNbKbGE3/h1xeIArY/yRI1IjDGYkLgDIankh+aDOtDRSr40LVlIHGL9+jEH1cVuxmbJ8NLL/1A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-websocket": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.21.tgz", - "integrity": "sha512-yr+5+C7v9R55sAJ89A55Wrm7wIKPVn5cm6J3Hztnd5s/iwEUKxyJqCnIxJu4fVXgG9XBQD1Jc4rsWC1ozahJjA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/fetch-http-handler": "^5.4.3", - "@smithy/signature-v4": "^5.4.2", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.11.tgz", - "integrity": "sha512-nWXXJ1r/r8N2Gw1pWolRgED38/A9A8DHR2ETWIv220zh4PZHcybbR4hUVWWktmNXTRHzDJwRluapHn0rZxuoqA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/signature-v4-multi-region": "^3.996.28", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/fetch-http-handler": "^5.4.3", - "@smithy/node-http-handler": "^4.7.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.28", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.28.tgz", - "integrity": "sha512-qs9z5LqXO/CZC2Lg9SGKpoLU8Rhi+m2pFKZqfO9pytX1clc0katqtsDNupJxFy0xT9wsZSPzM2v1y+/H/zfp5Q==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/signature-v4": "^5.4.2", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/token-providers": { - "version": "3.1053.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1053.0.tgz", - "integrity": "sha512-laSwHLYMMrXQRl2mFDXszF43m/F4pKWyGr7hCLfJmV8rn8c6CnI/hp/bf/Gn7gLcjz0SY4evd7SBpqtnIhzA/A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.13", - "@aws-sdk/nested-clients": "^3.997.11", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/types": { - "version": "3.973.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.9.tgz", - "integrity": "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/util-locate-window": { - "version": "3.965.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", - "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.25", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.25.tgz", - "integrity": "sha512-GH+Kjz4nPKWKHnsiQpnhP1MJdTGIcK4rAka6tzakgjjUkVgNsmPeEbbRAf09SzS1hjGu6duGHCBsxYke0BhHjQ==", + "version": "3.972.49", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.49.tgz", + "integrity": "sha512-EfJF/1Fh9mI4pZyoheU2RY9xUhTcugIZNkD63+orXMkYj/QXacJNbKVDUK90Yv5hE+aX+rt9J/EZ9Qr3vKOa7g==", "license": "Apache-2.0", "dependencies": { - "@nodable/entities": "2.1.0", - "@smithy/types": "^4.14.2", - "fast-xml-parser": "5.7.3", + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/nested-clients": "^3.997.17", + "@aws-sdk/types": "^3.973.11", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws/lambda-invoke-store": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", - "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@borewit/text-codec": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", - "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@cacheable/memory": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.9.tgz", - "integrity": "sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA==", - "license": "MIT", - "dependencies": { - "@cacheable/utils": "^2.4.1", - "@keyv/bigmap": "^1.3.1", - "hookified": "^1.15.1", - "keyv": "^5.6.0" - } - }, - "node_modules/@cacheable/node-cache": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@cacheable/node-cache/-/node-cache-1.7.6.tgz", - "integrity": "sha512-6Omk2SgNnjtxB5f/E6bTIWIt5xhdpx39fGNRQgU9lojvRxU68v+qY+SXXLsp3ZGukqoPjsK21wZ6XABFr/Ge3A==", - "license": "MIT", - "dependencies": { - "cacheable": "^2.3.1", - "hookified": "^1.14.0", - "keyv": "^5.5.5" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@cacheable/utils": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz", - "integrity": "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==", - "license": "MIT", - "dependencies": { - "hashery": "^1.5.1", - "keyv": "^5.6.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@google/genai": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", - "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "google-auth-library": "^10.3.0", - "p-retry": "^4.6.2", - "protobufjs": "^7.5.4", - "ws": "^8.18.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.25.2" - }, - "peerDependenciesMeta": { - "@modelcontextprotocol/sdk": { - "optional": true - } - } - }, - "node_modules/@googleworkspace/cli": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@googleworkspace/cli/-/cli-0.8.1.tgz", - "integrity": "sha512-NoHK7gQwZ5En95dzg+jL37UObsPX3xizzAyjzcM1DxFYzRhQbqdRTxK0di9a34MFCrNCluTem2Lyx6yKBrpeqg==", - "hasInstallScript": true, - "hasShrinkwrap": true, - "license": "Apache-2.0", - "dependencies": { - "axios": "^1.13.5", - "axios-proxy-builder": "^0.1.2", - "console.table": "^0.10.0", - "detect-libc": "^2.1.2", - "rimraf": "^6.1.3" - }, - "bin": { - "gws": "run-gws.js" - }, - "engines": { - "node": ">=14", - "npm": ">=6" - } - }, - "node_modules/@googleworkspace/cli/node_modules/@isaacs/cliui": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", - "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@googleworkspace/cli/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/@googleworkspace/cli/node_modules/axios": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/@googleworkspace/cli/node_modules/axios-proxy-builder": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/axios-proxy-builder/-/axios-proxy-builder-0.1.2.tgz", - "integrity": "sha512-6uBVsBZzkB3tCC8iyx59mCjQckhB8+GQrI9Cop8eC7ybIsvs/KtnNgEBfRMSEa7GqK2VBGUzgjNYMdPIfotyPA==", - "license": "MIT", - "dependencies": { - "tunnel": "^0.0.6" - } - }, - "node_modules/@googleworkspace/cli/node_modules/balanced-match": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz", - "integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==", - "license": "MIT", - "dependencies": { - "jackspeak": "^4.2.3" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@googleworkspace/cli/node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@googleworkspace/cli/node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@googleworkspace/cli/node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/@googleworkspace/cli/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/@googleworkspace/cli/node_modules/console.table": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/console.table/-/console.table-0.10.0.tgz", - "integrity": "sha512-dPyZofqggxuvSf7WXvNjuRfnsOk1YazkVP8FdxH4tcH2c37wc79/Yl6Bhr7Lsu00KMgy2ql/qCMuNu8xctZM8g==", - "license": "MIT", - "dependencies": { - "easy-table": "1.1.0" - }, - "engines": { - "node": "> 0.10" - } - }, - "node_modules/@googleworkspace/cli/node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "license": "MIT", - "optional": true, - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@googleworkspace/cli/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/@googleworkspace/cli/node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/@googleworkspace/cli/node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@googleworkspace/cli/node_modules/easy-table": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.1.0.tgz", - "integrity": "sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA==", - "license": "MIT", - "optionalDependencies": { - "wcwidth": ">=1.0.1" - } - }, - "node_modules/@googleworkspace/cli/node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@googleworkspace/cli/node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@googleworkspace/cli/node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@googleworkspace/cli/node_modules/es-set-tostringtag": { - "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==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@googleworkspace/cli/node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/@googleworkspace/cli/node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "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/@googleworkspace/cli/node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@googleworkspace/cli/node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@googleworkspace/cli/node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@googleworkspace/cli/node_modules/glob": { - "version": "13.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.3.tgz", - "integrity": "sha512-/g3B0mC+4x724v1TgtBlBtt2hPi/EWptsIAmXUx9Z2rvBYleQcsrmaOzd5LyL50jf/Soi83ZDJmw2+XqvH/EeA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.0", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@googleworkspace/cli/node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@googleworkspace/cli/node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@googleworkspace/cli/node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@googleworkspace/cli/node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@googleworkspace/cli/node_modules/jackspeak": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", - "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^9.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@googleworkspace/cli/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@googleworkspace/cli/node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@googleworkspace/cli/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/@googleworkspace/cli/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/@googleworkspace/cli/node_modules/minimatch": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.0.tgz", - "integrity": "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@googleworkspace/cli/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/@googleworkspace/cli/node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, - "node_modules/@googleworkspace/cli/node_modules/path-scurry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@googleworkspace/cli/node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", - "extraneous": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/@googleworkspace/cli/node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/@googleworkspace/cli/node_modules/rimraf": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", - "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "glob": "^13.0.3", - "package-json-from-dist": "^1.0.1" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@googleworkspace/cli/node_modules/tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", - "license": "MIT", - "engines": { - "node": ">=0.6.11 <=0.7.0 || >=0.7.3" - } - }, - "node_modules/@googleworkspace/cli/node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "license": "MIT", - "optional": true, - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/@grpc/grpc-js": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", - "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/proto-loader": "^0.8.0", - "@js-sdsl/ordered-map": "^4.4.2" - }, - "engines": { - "node": ">=12.10.0" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", - "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", - "license": "Apache-2.0", - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.5.5", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@hapi/boom": { - "version": "9.1.4", - "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.4.tgz", - "integrity": "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "9.x.x" - } - }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.52", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.52.tgz", + "integrity": "sha512-7QX+PbyiWBEOVipJq8Nke/TqXT6lAPLE7fvTaopa39/IVWuLfS+Fzdy71sZJONf/mLGgmtj6aU17+REw3+aRrw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.44", + "@aws-sdk/credential-provider-http": "^3.972.46", + "@aws-sdk/credential-provider-ini": "^3.972.50", + "@aws-sdk/credential-provider-process": "^3.972.44", + "@aws-sdk/credential-provider-sso": "^3.972.49", + "@aws-sdk/credential-provider-web-identity": "^3.972.49", + "@aws-sdk/types": "^3.973.11", + "@smithy/core": "^3.24.6", + "@smithy/credential-provider-imds": "^4.3.7", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.44", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.44.tgz", + "integrity": "sha512-V+UUhZpRP7QDRhi+qgBDisM9tUBnYmMje8Bk77A6MZsfeGeGdMsQXmaHP1CDYFcept0o/Rz5g2Y0TMeVlG9dzg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/types": "^3.973.11", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.49", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.49.tgz", + "integrity": "sha512-9QqOYGuh5tZ76OzaT68kwI78AH+5lS/uZGGvkfxb3fc8FzRrIz2jOufNTliEBEeSAwmgK2rWLNsK+IB3zbtNPA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/nested-clients": "^3.997.17", + "@aws-sdk/token-providers": "3.1063.0", + "@aws-sdk/types": "^3.973.11", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.49", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.49.tgz", + "integrity": "sha512-IYx1lN38MnnPXv+NBLpuATu0cZakbZ321TAfjW+aVkw7HIJF38YnEwdeEO55MSl3pl7hIX1IvvnD6EmnAzmAJw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/nested-clients": "^3.997.17", + "@aws-sdk/types": "^3.973.11", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], + "node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.20.tgz", + "integrity": "sha512-qr/S1iFCDIXlZwlZPaCqjKcHbJFr9scIFUhbh2+SrwPXZvRhyOUWjVDJpp8xoU4qrrMR0PqK1Yw5C2sSj7xAyw==", "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "dependencies": { + "@aws-sdk/types": "^3.973.11", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], + "node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.16.tgz", + "integrity": "sha512-KR2Gdui/QLbkdG9FxW3vk/vIa8KiDP5vQBNERo7MmlPHjn23GXJ53Cq5P/ok7/ALbTUiYZ78DiBHoDcvzPWvgQ==", "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "dependencies": { + "@aws-sdk/types": "^3.973.11", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], + "node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.26.tgz", + "integrity": "sha512-foM3KvxGBHY9lRIm6C9JJJ5haodtXfJPPgJQcv5/c4A2pN4I7tlnOjh1o2d8Il1Y/j6GWOw3YeIYc2/VYjtGVQ==", "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "dependencies": { + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/types": "^3.973.11", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.17.tgz", + "integrity": "sha512-lDRgraoTfKRawUyc176Ow93mrNrOho/x+EoK4C+lKU+vKkHWhNhzvSMVAx0WEJUJoeQxxDN5ZdKMfiGEyNejig==", "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/signature-v4-multi-region": "^3.996.32", + "@aws-sdk/types": "^3.973.11", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.32", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.32.tgz", + "integrity": "sha512-llvApLcsWtmRFhG2wT3WIp1CmDeRaIYutqty1ZZXoMzK7TiJ6MOLOimk9eXUS8PwgG4ew4pa4QAbt0lfhn++1w==", "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "dependencies": { + "@aws-sdk/types": "^3.973.11", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], + "node_modules/@aws-sdk/token-providers": { + "version": "3.1063.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1063.0.tgz", + "integrity": "sha512-nYDaWWdzjKiDP5xj8k4oUgcYd4WPgzfAOgdU5vJsaqH/07Dfvm7ffisHCFJ+NEl7kUC9JEIUxh0kznvenbo3NQ==", "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "dependencies": { + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/nested-clients": "^3.997.17", + "@aws-sdk/types": "^3.973.11", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], + "node_modules/@aws-sdk/types": { + "version": "3.973.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.11.tgz", + "integrity": "sha512-YjS0qFuECClRh4qhEyW8XagW0fwEPBeZ1cfsW/gU73Kh/ExFILxbzxOfPCmzF/2DwEvhvsHYt0b0qnvStwKYrg==", "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "dependencies": { + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.6.tgz", + "integrity": "sha512-ZfHjfwSzeXj+Lg9AK5ZNmeDkXev6V+w2tn1t4kgDdRtUaRCthepTQiFwbD06EF9oNGH4LaLg+Mb6U16Ypv5bSw==", "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "dependencies": { + "tslib": "^2.6.2" }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "peer": true, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.28", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.28.tgz", + "integrity": "sha512-lI/l3c/vPvsxmspzV63NfS3x9q4CkMmdhJy4QiM+NThAufVkDvi/PZZQ6xETnICL0UD7jI808pY83gllf86RFg==", + "license": "Apache-2.0", "dependencies": { - "@emnapi/runtime": "^1.7.0" + "@smithy/types": "^4.14.3", + "fast-xml-parser": "5.7.3", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=20.0.0" } }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "peer": true, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18.0.0" } }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "peer": true, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.0.0" }, - "funding": { - "url": "https://opencollective.com/libvips" + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } } }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "peer": true, + "node_modules/@grpc/grpc-js": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" }, - "funding": { - "url": "https://opencollective.com/libvips" + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" } }, "node_modules/@js-sdsl/ordered-map": { @@ -1667,28 +545,6 @@ "url": "https://opencollective.com/js-sdsl" } }, - "node_modules/@keyv/bigmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", - "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", - "license": "MIT", - "dependencies": { - "hashery": "^1.4.0", - "hookified": "^1.15.0" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "keyv": "^5.6.0" - } - }, - "node_modules/@keyv/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", - "license": "MIT" - }, "node_modules/@mariozechner/pi-agent-core": { "version": "0.70.6", "resolved": "https://registry.npmjs.org/@mariozechner/pi-agent-core/-/pi-agent-core-0.70.6.tgz", @@ -1730,9 +586,9 @@ } }, "node_modules/@mistralai/mistralai": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", - "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.5.tgz", + "integrity": "sha512-ATbWzKkNzNAZ+gtw9MI/c/ULTMG80tKUiRNIbQFfg4OP0uEZZpTfXZeBCNfs5Dq0uqMQ/tQWc4o6RRJQtMrpDA==", "license": "Apache-2.0", "dependencies": { "ws": "^8.18.0", @@ -1741,9 +597,9 @@ } }, "node_modules/@nodable/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz", + "integrity": "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==", "funding": [ { "type": "github", @@ -2362,10 +1218,9 @@ } }, "node_modules/@opentelemetry/otlp-transformer/node_modules/protobufjs": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.4.2.tgz", - "integrity": "sha512-64rfNzkWOZAIazXzpBFPWq6F9up6gMvTzjE2oWIzApx2N/dqVUEE7+bCn2+40780dFVtKOUab8QfxJ6KJDWbqA==", - "hasInstallScript": true, + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.6.0.tgz", + "integrity": "sha512-PIOO89BMGMXGz2333TVv/OqPNVWm7w30ll/4FtLbtLBaonzJMYwTbAZSSlobjIy9MoUgIAxSVUpK7aP7EpTtkg==", "license": "BSD-3-Clause", "dependencies": { "long": "^5.3.2" @@ -2691,12 +1546,6 @@ "node": ">=14" } }, - "node_modules/@pinojs/redact": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", - "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", - "license": "MIT" - }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -2767,13 +1616,13 @@ "license": "MIT" }, "node_modules/@smithy/core": { - "version": "3.24.4", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.4.tgz", - "integrity": "sha512-3UNRKEyQyAgVgM0LGlerCLm+ChZWZ1GPfde+jBEW6bm6bSBGU1p0EbblaUV3unbhwvidjLA5Zs3sOs7mnZwvAw==", + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.6.tgz", + "integrity": "sha512-wBXDRup6UU97VKyaiRo8AssnfStPtG0oAAfpq/bC0a1YYau8pM86YB4kM6ccoVi1mS8l/UHbn9oDM+7uozr/ug==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.14.2", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -2781,13 +1630,13 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.4.tgz", - "integrity": "sha512-vKW0MEFRU4Y3MkVZUkpJm+g9qyPGLCXhc0YLggUdSdBB4g7IaSSsCE75P9rBXyWHrXY1UYSQUl8/DwsTR7QciA==", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.8.tgz", + "integrity": "sha512-5cAM+KZC02sTqDt6NaLXyu50M/GNMd1eTzDVR8Lb0BBsVtu7RWHo47VPPEEv1vt3Yub6uzr+M5FHC+GtoT0USg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.24.4", - "@smithy/types": "^4.14.2", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -2795,13 +1644,13 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.4.tgz", - "integrity": "sha512-qM7AUKI4G6d7lNgaZD3lA1tWSolh5r6gcixfTZAPstVURfjIbvreVTPz+994M0yC3HbX4YYhDRgr31Xy3XwWOQ==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.6.tgz", + "integrity": "sha512-FEwEYJ1jlBKdhe9TPzfghEi1bP55ZeEImlDkEa62bBBYzUcnB6RUCyuiS2mqKt6ZVjUbBgcNhzfIctH+Hevx9g==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.24.4", - "@smithy/types": "^4.14.2", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -2821,13 +1670,13 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.4.tgz", - "integrity": "sha512-HIeF+1vrDGzPkkv39Hj2vlHSXHY3p958jd/8ZnePIY6+ZOsQX8coyEUKO5yQu4r0bQIVsbpotVIrXXwyycMStQ==", + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.7.tgz", + "integrity": "sha512-ZAFvHXrEk6K180EVhmZVg8GU5pUH5BSFqRs27JW3j1qEFx9YyYwWFx17x/MHcjALYimGAji7qEOlF1++be+G5A==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.24.4", - "@smithy/types": "^4.14.2", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -2835,13 +1684,13 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.4.tgz", - "integrity": "sha512-e5UtkMvsatzBfbeBZjEOt0k0Z3BEsjTFL/n6fdO5vtBLe67tdy0dX7xw2DU7uZ3acwoHyeCqpU2Fzb7pxwHb6Q==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.6.tgz", + "integrity": "sha512-Ojg4B6oIDlIr1R86xCDJt1zJWnYa0VINmqdjfe9qxWjdRivHalZ3iSlQgVqYbW0MdpFOC5XfHEWsnbmdnpIILQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.24.4", - "@smithy/types": "^4.14.2", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -2849,9 +1698,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", - "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.3.tgz", + "integrity": "sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -2886,29 +1735,6 @@ "node": ">=14.0.0" } }, - "node_modules/@tokenizer/inflate": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", - "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "token-types": "^6.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "license": "MIT" - }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -2923,9 +1749,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", - "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "version": "22.19.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.20.tgz", + "integrity": "sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -2944,16 +1770,6 @@ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "license": "MIT" }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -3026,64 +1842,6 @@ "node": ">=4" } }, - "node_modules/async-mutex": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", - "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/baileys": { - "version": "7.0.0-rc13", - "resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc13.tgz", - "integrity": "sha512-v8k74K8B5R7WNYGa26MyJAYEu3Wc4BSuK01QaK8lr30lhE8Nga31nWNu8KN0NDDt+Fsvkq4SQFFI8Q13ghjKmA==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@cacheable/node-cache": "^1.4.0", - "@hapi/boom": "^9.1.3", - "async-mutex": "^0.5.0", - "libsignal": "^6.0.0", - "lru-cache": "^11.1.0", - "music-metadata": "^11.12.3", - "p-queue": "^9.0.0", - "pino": "^9.6", - "protobufjs": "^7.5.6", - "whatsapp-rust-bridge": "0.5.4", - "ws": "^8.13.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "audio-decode": "^2.1.3", - "jimp": "^1.6.1", - "link-preview-js": "^3.0.0", - "sharp": "*" - }, - "peerDependenciesMeta": { - "audio-decode": { - "optional": true - }, - "jimp": { - "optional": true - }, - "link-preview-js": { - "optional": true - } - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -3134,19 +1892,6 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, - "node_modules/cacheable": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.5.tgz", - "integrity": "sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==", - "license": "MIT", - "dependencies": { - "@cacheable/memory": "^2.0.8", - "@cacheable/utils": "^2.4.1", - "hookified": "^1.15.0", - "keyv": "^5.6.0", - "qified": "^0.10.1" - } - }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -3197,21 +1942,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/curve25519-js": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/curve25519-js/-/curve25519-js-0.0.4.tgz", - "integrity": "sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w==", - "license": "MIT" - }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -3252,15 +1982,6 @@ "node": ">= 14" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -3337,12 +2058,6 @@ "node": ">=0.10.0" } }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "license": "MIT" - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -3409,24 +2124,6 @@ "node": "^12.20 || >= 14.13" } }, - "node_modules/file-type": { - "version": "21.3.4", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", - "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", - "license": "MIT", - "dependencies": { - "@tokenizer/inflate": "^0.4.1", - "strtok3": "^10.3.4", - "token-types": "^6.1.1", - "uint8array-extras": "^1.4.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -3440,9 +2137,9 @@ } }, "node_modules/gaxios": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", - "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.5.tgz", + "integrity": "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==", "license": "Apache-2.0", "dependencies": { "extend": "^3.0.2", @@ -3500,9 +2197,9 @@ } }, "node_modules/google-auth-library": { - "version": "10.6.2", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", - "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.7.0.tgz", + "integrity": "sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ==", "license": "Apache-2.0", "dependencies": { "base64-js": "^1.3.0", @@ -3525,24 +2222,6 @@ "node": ">=14" } }, - "node_modules/hashery": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", - "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", - "license": "MIT", - "dependencies": { - "hookified": "^1.15.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/hookified": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", - "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", - "license": "MIT" - }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -3569,26 +2248,6 @@ "node": ">= 14" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/import-in-the-middle": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.0.1.tgz", @@ -3623,9 +2282,19 @@ } }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -3677,25 +2346,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/keyv": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", - "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", - "license": "MIT", - "dependencies": { - "@keyv/serialize": "^1.1.1" - } - }, - "node_modules/libsignal": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/libsignal/-/libsignal-6.0.0.tgz", - "integrity": "sha512-d/5V3YFtDljbFMufz4ncyUYGYhJl+vzAe+c2EFFBQ6bz1h8Q3IOMEGXYMzlibU60I+e8GagMMpji18iez3P1hA==", - "license": "GPL-3.0", - "dependencies": { - "curve25519-js": "^0.0.4", - "protobufjs": "^7.5.5" - } - }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -3709,21 +2359,12 @@ "license": "Apache-2.0" }, "node_modules/lru-cache": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", - "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "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", + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", "engines": { - "node": ">= 0.8" + "node": ">=12" } }, "node_modules/module-details-from-path": { @@ -3738,37 +2379,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/music-metadata": { - "version": "11.12.3", - "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.12.3.tgz", - "integrity": "sha512-n6hSTZkuD59qWgHh6IP5dtDlDZQXoxk/bcA85Jywg8Z1iFrlNgl2+GTFgjZyn52W5UgQpV42V4XqrQZZAMbZTQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - }, - { - "type": "buymeacoffee", - "url": "https://buymeacoffee.com/borewit" - } - ], - "license": "MIT", - "dependencies": { - "@borewit/text-codec": "^0.2.2", - "@tokenizer/token": "^0.3.0", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "file-type": "^21.3.1", - "media-typer": "^1.1.0", - "strtok3": "^10.3.4", - "token-types": "^6.1.2", - "uint8array-extras": "^1.5.0", - "win-guid": "^0.2.1" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/netmask": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", @@ -3828,15 +2438,6 @@ "url": "https://opencollective.com/node-fetch" } }, - "node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/openai": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", @@ -3858,22 +2459,6 @@ } } }, - "node_modules/p-queue": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.0.tgz", - "integrity": "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==", - "license": "MIT", - "dependencies": { - "eventemitter3": "^5.0.4", - "p-timeout": "^7.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-retry": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", @@ -3887,18 +2472,6 @@ "node": ">=8" } }, - "node_modules/p-timeout": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", - "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/pac-proxy-agent": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", @@ -3952,63 +2525,10 @@ "node": ">=14.0.0" } }, - "node_modules/pino": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", - "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", - "license": "MIT", - "dependencies": { - "@pinojs/redact": "^0.4.0", - "atomic-sleep": "^1.0.0", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^5.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^4.0.1", - "thread-stream": "^3.0.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", - "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", - "license": "MIT", - "dependencies": { - "split2": "^4.0.0" - } - }, - "node_modules/pino-std-serializers": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", - "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", - "license": "MIT" - }, - "node_modules/process-warning": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", - "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, "node_modules/protobufjs": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.1.tgz", - "integrity": "sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.2.tgz", + "integrity": "sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -4048,54 +2568,12 @@ "node": ">= 14" } }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, - "node_modules/qified": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", - "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", - "license": "MIT", - "dependencies": { - "hookified": "^2.1.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/qified/node_modules/hookified": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", - "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", - "license": "MIT" - }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "license": "MIT" - }, - "node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4147,73 +2625,6 @@ ], "license": "MIT" }, - "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/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -4252,15 +2663,6 @@ "node": ">= 14" } }, - "node_modules/sonic-boom": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", - "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -4271,15 +2673,6 @@ "node": ">=0.10.0" } }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -4318,49 +2711,6 @@ ], "license": "MIT" }, - "node_modules/strtok3": { - "version": "10.3.5", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", - "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/thread-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", - "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", - "license": "MIT", - "dependencies": { - "real-require": "^0.2.0" - } - }, - "node_modules/token-types": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", - "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", - "license": "MIT", - "dependencies": { - "@borewit/text-codec": "^0.2.1", - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/ts-algebra": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", @@ -4374,9 +2724,9 @@ "license": "0BSD" }, "node_modules/typebox": { - "version": "1.1.38", - "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", - "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.2.1.tgz", + "integrity": "sha512-0upGv6+mxJR7/Wc7yoxjc/U6SjOk2aNDNzbihYacSHh+JfOsf28IJ8ggW4/3tRlDKfbInvEDPVneEywjOWYCzw==", "license": "MIT" }, "node_modules/typescript": { @@ -4393,22 +2743,10 @@ "node": ">=14.17" } }, - "node_modules/uint8array-extras": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", - "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/undici": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.2.tgz", + "integrity": "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -4439,18 +2777,6 @@ "node": ">= 8" } }, - "node_modules/whatsapp-rust-bridge": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/whatsapp-rust-bridge/-/whatsapp-rust-bridge-0.5.4.tgz", - "integrity": "sha512-yYO1qSs0Fe7tGtnxOFHomocUD6IZtoAgmA4oDFyGIRZ67D3QZk3w7swA6XXFXNQngiyrg2k7tul6IrM3eUFh7A==", - "license": "MIT" - }, - "node_modules/win-guid": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/win-guid/-/win-guid-0.2.1.tgz", - "integrity": "sha512-gEIQU4mkgl2OPeoNrWflcJFJ3Ae2BPd4eCsHHA/XikslkIVms/nHhvnvzIZV7VLmBvtFlDOzLt9rrZT+n6D67A==", - "license": "MIT" - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 1ca9522..00aa77c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@open-gitagent/gitagent", - "version": "1.5.2", + "version": "2.0.0", "description": "A universal git-native multimodal always learning AI Agent (TinyHuman)", "author": "shreyaskapale", "license": "MIT", @@ -20,7 +20,10 @@ "type": "module", "files": [ "dist", - "README.md" + "!dist/voice", + "!dist/composio", + "README.md", + "LICENSE" ], "main": "./dist/exports.js", "types": "./dist/exports.d.ts", @@ -37,7 +40,7 @@ } }, "scripts": { - "build": "tsc && cp src/voice/ui.html dist/voice/", + "build": "tsc", "dev": "tsc --watch", "start": "node dist/index.js", "test": "node --test test/*.test.ts --experimental-strip-types" @@ -46,7 +49,6 @@ "node": ">=20" }, "dependencies": { - "@googleworkspace/cli": "^0.8.1", "@mariozechner/pi-agent-core": "^0.70.2", "@mariozechner/pi-ai": "^0.70.2", "@opentelemetry/api": "^1.9.0", @@ -60,10 +62,8 @@ "@opentelemetry/sdk-trace-node": "^2.7.0", "@opentelemetry/semantic-conventions": "^1.40.0", "@sinclair/typebox": "^0.34.41", - "baileys": "^7.0.0-rc.9", "js-yaml": "^4.1.0", "node-cron": "^3.0.3", - "ws": "^8.19.0", "yaml": "^2.8.2" }, "peerDependencies": { @@ -78,7 +78,6 @@ "@types/js-yaml": "^4.0.9", "@types/node": "^22.0.0", "@types/node-cron": "^3.0.11", - "@types/ws": "^8.18.1", "typescript": "^5.7.0" } } diff --git a/src/voice/adapter.ts b/src/adapter.ts similarity index 100% rename from src/voice/adapter.ts rename to src/adapter.ts diff --git a/src/voice/chat-history.ts b/src/chat-history.ts similarity index 99% rename from src/voice/chat-history.ts rename to src/chat-history.ts index 37d8401..36f98f8 100644 --- a/src/voice/chat-history.ts +++ b/src/chat-history.ts @@ -1,7 +1,7 @@ import { appendFileSync, readFileSync, unlinkSync, mkdirSync, writeFileSync } from "fs"; import { join } from "path"; import type { ServerMessage } from "./adapter.js"; -import { query } from "../sdk.js"; +import { query } from "./sdk.js"; /** Types we skip — too large or ephemeral */ const SKIP_TYPES = new Set(["audio_delta", "agent_thinking"]); diff --git a/src/composio/adapter.ts b/src/composio/adapter.ts deleted file mode 100644 index 5884815..0000000 --- a/src/composio/adapter.ts +++ /dev/null @@ -1,119 +0,0 @@ -// Converts Composio tools into GCToolDefinition[] for injection into query() - -import type { GCToolDefinition } from "../sdk-types.js"; -import { ComposioClient, type ComposioToolkit, type ComposioConnection, type ComposioTool } from "./client.js"; - -interface ComposioAdapterOptions { - apiKey: string; - userId?: string; -} - -export class ComposioAdapter { - private client: ComposioClient; - private userId: string; - private cachedTools: GCToolDefinition[] | null = null; - private cacheExpiry = 0; - private static CACHE_TTL = 30_000; // 30s - - constructor(opts: ComposioAdapterOptions) { - this.client = new ComposioClient(opts.apiKey); - this.userId = opts.userId ?? "default"; - } - - // Core — returns all tools for connected toolkits (cached) - async getTools(): Promise { - const now = Date.now(); - if (this.cachedTools && now < this.cacheExpiry) return this.cachedTools; - - const connections = await this.client.listConnections(this.userId); - if (connections.length === 0) return []; - - // Deduplicate toolkit slugs - const slugs = [...new Set(connections.map((c) => c.toolkitSlug))]; - - // Fetch tools for each connected toolkit in parallel - const toolsBySlug = await Promise.all( - slugs.map((slug) => this.client.listTools(slug).catch(() => [] as ComposioTool[])), - ); - - const tools: GCToolDefinition[] = []; - for (const toolGroup of toolsBySlug) { - for (const t of toolGroup) { - tools.push(this.toGCTool(t)); - } - } - - this.cachedTools = tools; - this.cacheExpiry = now + ComposioAdapter.CACHE_TTL; - return tools; - } - - // Dynamically fetch only the relevant tools for a user query (semantic search) - async getToolsForQuery(query: string, limit = 15): Promise { - const connections = await this.client.listConnections(this.userId); - if (connections.length === 0) return []; - - const slugs = [...new Set(connections.map((c) => c.toolkitSlug))]; - const tools = await this.client.searchTools(query, slugs, limit); - - // Sort: direct-action tools first (SEND, CREATE, LIST), drafts last - tools.sort((a, b) => { - const aIsDraft = a.slug.includes("DRAFT"); - const bIsDraft = b.slug.includes("DRAFT"); - if (aIsDraft !== bIsDraft) return aIsDraft ? 1 : -1; - return 0; - }); - - return tools.map((t) => this.toGCTool(t)); - } - - // Returns deduplicated slugs of all connected toolkits - async getConnectedToolkitSlugs(): Promise { - const connections = await this.client.listConnections(this.userId); - return [...new Set(connections.map((c) => c.toolkitSlug))]; - } - - // Management endpoints — proxied for server routes - - async getToolkits(): Promise { - return this.client.listToolkits(this.userId); - } - - async connect( - toolkit: string, - redirectUrl?: string, - ): Promise<{ connectionId: string; redirectUrl: string }> { - return this.client.initiateConnection(toolkit, this.userId, redirectUrl); - } - - async getConnections(): Promise { - return this.client.listConnections(this.userId); - } - - async disconnect(connectionId: string): Promise { - await this.client.deleteConnection(connectionId); - // Invalidate cache so tools refresh on next query - this.cachedTools = null; - } - - // ── Private ──────────────────────────────────────────────────────── - - private toGCTool(t: ComposioTool): GCToolDefinition { - const safeName = `composio_${t.toolkitSlug}_${t.slug}`.replace(/[^a-zA-Z0-9_]/g, "_"); - let description = `[Composio/${t.toolkitSlug}] ${t.description}`; - if (t.slug.includes("SEND_EMAIL")) { - description += " — USE THIS to send emails directly."; - } else if (t.slug.includes("CREATE_EMAIL_DRAFT")) { - description += " — Only use when the user explicitly asks for a draft."; - } - return { - name: safeName, - description, - inputSchema: t.parameters, - handler: async (args: any) => { - const result = await this.client.executeTool(t.slug, this.userId, args); - return typeof result === "string" ? result : JSON.stringify(result); - }, - }; - } -} diff --git a/src/composio/client.ts b/src/composio/client.ts deleted file mode 100644 index 145d72e..0000000 --- a/src/composio/client.ts +++ /dev/null @@ -1,242 +0,0 @@ -// Composio REST API v3 client — zero dependencies, uses native fetch() - -const BASE_URL = "https://backend.composio.dev/api/v3"; - -// ── Types ──────────────────────────────────────────────────────────── - -export interface ComposioToolkit { - slug: string; - name: string; - description: string; - logo: string; - authSchemes: string[]; - noAuth: boolean; - connected: boolean; -} - -export interface ComposioConnection { - id: string; - toolkitSlug: string; - status: string; - createdAt: string; -} - -export interface ComposioTool { - name: string; - slug: string; - description: string; - toolkitSlug: string; - parameters: Record; -} - -// ── Client ─────────────────────────────────────────────────────────── - -export class ComposioClient { - private apiKey: string; - // Cache auth config IDs so we don't recreate them every connect - private authConfigCache = new Map(); - - constructor(apiKey: string) { - this.apiKey = apiKey; - } - - // List available toolkits, optionally merging connection status for a user - async listToolkits(userId?: string): Promise { - const resp = await this.request("GET", "/toolkits"); - - const toolkits: any[] = Array.isArray(resp) ? resp : (resp.items ?? resp.toolkits ?? []); - - let connectedSlugs = new Set(); - if (userId) { - try { - const conns = await this.listConnections(userId); - connectedSlugs = new Set(conns.map((c) => c.toolkitSlug)); - } catch { - // If connections fail, just show all as disconnected - } - } - - return toolkits.map((tk: any) => ({ - slug: tk.slug ?? "", - name: tk.name ?? tk.slug ?? "", - description: tk.meta?.description ?? tk.description ?? "", - logo: tk.meta?.logo ?? tk.logo ?? "", - authSchemes: tk.auth_schemes ?? [], - noAuth: tk.no_auth ?? false, - connected: connectedSlugs.has(tk.slug ?? ""), - })); - } - - // Search tools across connected toolkits by natural language query - // Makes parallel per-toolkit requests since the API doesn't support comma-separated toolkit_slug with query - async searchTools(query: string, toolkitSlugs?: string[], limit = 10): Promise { - const mapTool = (t: any): ComposioTool => ({ - name: t.name ?? t.enum ?? "", - slug: t.slug ?? t.enum ?? t.name ?? "", - description: t.description ?? "", - toolkitSlug: t.toolkit?.slug ?? t.toolkit_slug ?? "", - parameters: t.input_parameters ?? t.parameters ?? t.inputParameters ?? {}, - }); - - if (!toolkitSlugs?.length) { - const params = new URLSearchParams({ query, limit: String(limit) }); - const resp = await this.request("GET", `/tools?${params}`); - const tools: any[] = Array.isArray(resp) ? resp : (resp.items ?? resp.tools ?? []); - return tools.map(mapTool); - } - - // Parallel per-toolkit search - const perToolkit = await Promise.all( - toolkitSlugs.map(async (slug) => { - try { - const params = new URLSearchParams({ query, toolkit_slug: slug, limit: String(limit) }); - const resp = await this.request("GET", `/tools?${params}`); - const tools: any[] = Array.isArray(resp) ? resp : (resp.items ?? resp.tools ?? []); - return tools.map(mapTool); - } catch { - return [] as ComposioTool[]; - } - }), - ); - - return perToolkit.flat().slice(0, limit); - } - - // List tools for a specific toolkit - async listTools(toolkitSlug: string): Promise { - const resp = await this.request( - "GET", - `/tools?toolkit_slug=${encodeURIComponent(toolkitSlug)}`, - ); - - const tools: any[] = Array.isArray(resp) ? resp : (resp.items ?? resp.tools ?? []); - - return tools.map((t: any) => ({ - name: t.name ?? t.enum ?? "", - slug: t.slug ?? t.enum ?? t.name ?? "", - description: t.description ?? "", - toolkitSlug, - parameters: t.input_parameters ?? t.parameters ?? t.inputParameters ?? {}, - })); - } - - // Get or create an auth config for a toolkit (needed before creating a connection) - async getOrCreateAuthConfig(toolkitSlug: string): Promise { - // Check cache first - const cached = this.authConfigCache.get(toolkitSlug); - if (cached) return cached; - - // Check if one already exists - const existing = await this.request( - "GET", - `/auth_configs?toolkit_slug=${encodeURIComponent(toolkitSlug)}`, - ); - const items: any[] = existing.items ?? []; - if (items.length > 0) { - const id = items[0].id ?? items[0].auth_config?.id; - if (id) { - this.authConfigCache.set(toolkitSlug, id); - return id; - } - } - - // Create a new one with Composio-managed auth - const created = await this.request("POST", "/auth_configs", { - toolkit: { slug: toolkitSlug }, - auth_scheme: "OAUTH2", - use_composio_auth: true, - }); - - const id = created.auth_config?.id ?? created.id ?? ""; - if (id) this.authConfigCache.set(toolkitSlug, id); - return id; - } - - // Start OAuth connection flow (two-step: ensure auth config, then create connection) - async initiateConnection( - toolkitSlug: string, - userId: string, - redirectUrl?: string, - ): Promise<{ connectionId: string; redirectUrl: string }> { - const authConfigId = await this.getOrCreateAuthConfig(toolkitSlug); - if (!authConfigId) { - throw new Error(`Failed to get auth config for toolkit: ${toolkitSlug}`); - } - - const body: Record = { - auth_config: { id: authConfigId }, - connection: { - user_id: userId, - ...(redirectUrl ? { callback_url: redirectUrl } : {}), - }, - }; - - const resp = await this.request("POST", "/connected_accounts", body); - return { - connectionId: resp.id ?? "", - redirectUrl: resp.redirect_url ?? resp.redirect_uri ?? resp.redirectUrl ?? resp.redirectUri ?? "", - }; - } - - // List active connections for a user - async listConnections(userId: string): Promise { - const resp = await this.request( - "GET", - `/connected_accounts?user_ids=${encodeURIComponent(userId)}&statuses=ACTIVE`, - ); - - const items: any[] = Array.isArray(resp) ? resp : (resp.items ?? resp.connections ?? []); - return items.map((c: any) => ({ - id: c.id ?? "", - toolkitSlug: c.toolkit?.slug ?? c.toolkit_slug ?? c.appUniqueId ?? c.integrationId ?? "", - status: c.status ?? "ACTIVE", - createdAt: c.createdAt ?? c.created_at ?? "", - })); - } - - // Delete a connection - async deleteConnection(id: string): Promise { - await this.request("DELETE", `/connected_accounts/${encodeURIComponent(id)}`); - } - - // Execute a tool action - async executeTool( - toolSlug: string, - userId: string, - params: Record, - connectedAccountId?: string, - ): Promise { - const body: Record = { - arguments: params, - user_id: userId, - }; - if (connectedAccountId) body.connected_account_id = connectedAccountId; - - return this.request("POST", `/tools/execute/${encodeURIComponent(toolSlug)}`, body); - } - - // ── Private ──────────────────────────────────────────────────────── - - private async request(method: string, path: string, body?: any): Promise { - const url = `${BASE_URL}${path}`; - const headers: Record = { - "x-api-key": this.apiKey, - "Accept": "application/json", - }; - if (body) headers["Content-Type"] = "application/json"; - - const resp = await fetch(url, { - method, - headers, - body: body ? JSON.stringify(body) : undefined, - }); - - if (!resp.ok) { - const text = await resp.text().catch(() => ""); - throw new Error(`Composio API ${method} ${path} failed (${resp.status}): ${text}`); - } - - if (resp.status === 204) return undefined as T; - return resp.json() as Promise; - } -} diff --git a/src/composio/index.ts b/src/composio/index.ts deleted file mode 100644 index 8083d76..0000000 --- a/src/composio/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ComposioClient, type ComposioToolkit, type ComposioConnection, type ComposioTool } from "./client.js"; -export { ComposioAdapter } from "./adapter.js"; diff --git a/src/context.ts b/src/context.ts index 17ff2d5..965dd17 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,7 +1,7 @@ import { readFileSync, existsSync } from "fs"; import { join } from "path"; -import { loadHistory } from "./voice/chat-history.js"; -import type { ServerMessage } from "./voice/adapter.js"; +import { loadHistory } from "./chat-history.js"; +import type { ServerMessage } from "./adapter.js"; /** Token estimate: ~4 chars per token */ function estimateTokens(text: string): number { diff --git a/src/exports.ts b/src/exports.ts index 2748236..787d60f 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -37,9 +37,70 @@ export { createSandboxContext } from "./sandbox.js"; export type { LocalSession } from "./session.js"; export { initLocalSession } from "./session.js"; -// Voice -export type { VoiceAdapter, VoiceAdapterConfig, VoiceServerOptions } from "./voice/adapter.js"; -export { startVoiceServer } from "./voice/server.js"; +// Voice — startVoiceServer moved to the optional @open-gitagent/voice package +// in v2.0.0. The message-type protocol below stays in core because chat history +// and the standalone scheduler persist and emit these types. + +// Multimodal adapter protocol types (web-UI client/server message bus) +export type { + AdapterBackend, + ClientMessage, + ClientAudioMessage, + ClientVideoFrameMessage, + ClientTextMessage, + ClientFileMessage, + ServerMessage, + ServerAudioDelta, + ServerTranscript, + ServerAgentWorking, + ServerAgentDone, + ServerToolCall, + ServerToolResult, + ServerAgentThinking, + ServerError, + ServerInterrupt, + ServerFilesChanged, + ServerMemorySaving, + ServerLogEntry, + MultimodalAdapter, + MultimodalAdapterConfig, + VoiceServerOptions, + VoiceAdapter, + VoiceAdapterConfig, +} from "./adapter.js"; +export { DEFAULT_VOICE_INSTRUCTIONS } from "./adapter.js"; + +// Chat history persistence (used by voice and by any non-voice consumer) +export { + appendMessage, + loadHistory, + deleteHistory, + summarizeHistory, +} from "./chat-history.js"; + +// Symbols re-exported for @open-gitagent/voice (consumed via peer dependency). +// These are stable enough for the voice package to depend on; flagged here so +// future renames know they're part of the public-ish surface. +export { getVoiceContext, getAgentContext } from "./context.js"; +export { discoverSkills } from "./skills.js"; +export { + discoverWorkflows, + loadFlowDefinition, + saveFlowDefinition, + deleteFlowDefinition, +} from "./workflows.js"; +export { + discoverSchedules, + saveSchedule, + deleteSchedule, + updateScheduleMeta, +} from "./schedules.js"; +export { + startScheduler, + stopScheduler, + reloadSchedules, + executeScheduledJob, +} from "./schedule-runner.js"; // Plugin types export type { PluginManifest, PluginConfig, LoadedPlugin } from "./plugin-types.js"; diff --git a/src/index.ts b/src/index.ts index 2c09693..68ea4ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,7 +21,9 @@ import { homedir } from "os"; import { execSync } from "child_process"; import { initLocalSession } from "./session.js"; import type { LocalSession } from "./session.js"; -import { startVoiceServer } from "./voice/server.js"; +// Voice mode is shipped as an optional sibling package (@open-gitagent/voice). +// Imported dynamically below so the slim core has no static dependency on it — +// users without voice get a clean install + a clear error if they try --voice. import { handlePluginCommand } from "./plugin-cli.js"; import { context as otelContext } from "@opentelemetry/api"; import { @@ -402,8 +404,24 @@ async function main(): Promise { await initTelemetry({}); } - // Voice mode + // Voice mode — dynamically load the @open-gitagent/voice package. + // Core ships without voice so the published tarball stays slim and supply-chain + // scanners don't reject it. If the user passes --voice without voice installed, + // we print an install hint and exit cleanly. if (voice) { + let voiceMod: { startVoiceServer: (opts: any) => Promise<() => Promise> }; + try { + // @ts-ignore — peer/optional package, not in core's dependencies + voiceMod = await import("@open-gitagent/voice"); + } catch { + console.error(red("\nVoice mode lives in a separate optional package.")); + console.error("Install it once globally:\n"); + console.error(" " + bold("npm install -g @open-gitagent/voice") + "\n"); + console.error(dim("Then rerun: gitagent --voice -d ")); + console.error(dim("Or use install.sh, which installs both packages by default.")); + process.exit(1); + } + let adapterBackend: "openai-realtime" | "gemini-live"; let apiKey: string | undefined; @@ -421,7 +439,7 @@ async function main(): Promise { } } - const cleanup = await startVoiceServer({ + const cleanup = await voiceMod.startVoiceServer({ adapter: adapterBackend, adapterConfig: { apiKey }, agentDir: dir, diff --git a/src/schedule-runner.ts b/src/schedule-runner.ts index a2db8c8..9e677ce 100644 --- a/src/schedule-runner.ts +++ b/src/schedule-runner.ts @@ -2,7 +2,7 @@ import cron, { type ScheduledTask } from "node-cron"; import { discoverSchedules, updateScheduleMeta, type ScheduleDefinition } from "./schedules.js"; import { mkdirSync, appendFileSync } from "fs"; import { join } from "path"; -import type { ServerMessage } from "./voice/adapter.js"; +import type { ServerMessage } from "./adapter.js"; const dim = (s: string) => `\x1b[2m${s}\x1b[0m`; diff --git a/src/voice/gemini-live.ts b/src/voice/gemini-live.ts deleted file mode 100644 index 9b54757..0000000 --- a/src/voice/gemini-live.ts +++ /dev/null @@ -1,324 +0,0 @@ -import WebSocket from "ws"; -import { - DEFAULT_VOICE_INSTRUCTIONS, - type MultimodalAdapter, - type MultimodalAdapterConfig, - type ClientMessage, - type ServerMessage, -} from "./adapter.js"; - -const dim = (s: string) => `\x1b[2m${s}\x1b[0m`; - -/** - * Downsample 24kHz PCM (Int16LE) to 16kHz by linear interpolation (2 of every 3 samples). - * Input: base64-encoded 24kHz Int16LE. Output: base64-encoded 16kHz Int16LE. - */ -function downsample24kTo16k(base64_24k: string): string { - const binary = Buffer.from(base64_24k, "base64"); - const samples24 = new Int16Array(binary.buffer, binary.byteOffset, binary.byteLength / 2); - const outLength = Math.floor(samples24.length * 2 / 3); - const samples16 = new Int16Array(outLength); - - for (let i = 0; i < outLength; i++) { - // Map output index to fractional input index - const srcIdx = i * 1.5; - const lo = Math.floor(srcIdx); - const frac = srcIdx - lo; - const hi = Math.min(lo + 1, samples24.length - 1); - samples16[i] = Math.round(samples24[lo] * (1 - frac) + samples24[hi] * frac); - } - - return Buffer.from(samples16.buffer).toString("base64"); -} - -/** - * Upsample 16kHz PCM (Int16LE) to 24kHz by linear interpolation. - * Input: base64-encoded 16kHz Int16LE. Output: base64-encoded 24kHz Int16LE. - */ -function upsample16kTo24k(base64_16k: string): string { - const binary = Buffer.from(base64_16k, "base64"); - const samples16 = new Int16Array(binary.buffer, binary.byteOffset, binary.byteLength / 2); - const outLength = Math.floor(samples16.length * 3 / 2); - const samples24 = new Int16Array(outLength); - - for (let i = 0; i < outLength; i++) { - const srcIdx = i * (2 / 3); - const lo = Math.floor(srcIdx); - const frac = srcIdx - lo; - const hi = Math.min(lo + 1, samples16.length - 1); - samples24[i] = Math.round(samples16[lo] * (1 - frac) + samples16[hi] * frac); - } - - return Buffer.from(samples24.buffer).toString("base64"); -} - -export class GeminiLiveAdapter implements MultimodalAdapter { - private ws: WebSocket | null = null; - private config: MultimodalAdapterConfig; - private onMessage: ((msg: ServerMessage) => void) | null = null; - private toolHandler: ((query: string) => Promise) | null = null; - private setupDone = false; - - constructor(config: MultimodalAdapterConfig) { - this.config = config; - } - - async connect(opts: { - toolHandler: (query: string) => Promise; - onMessage: (msg: ServerMessage) => void; - }): Promise { - this.onMessage = opts.onMessage; - this.toolHandler = opts.toolHandler; - this.setupDone = false; - - const model = this.config.model || "models/gemini-2.5-flash-native-audio-preview"; - const url = `wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent?key=${this.config.apiKey}`; - - return new Promise((resolve, reject) => { - this.ws = new WebSocket(url); - - this.ws.on("open", () => { - console.log(dim("[voice] Connected to Gemini Multimodal Live")); - this.sendSetup(model); - }); - - this.ws.on("error", (err) => { - console.error(dim(`[voice] Gemini WS error: ${err.message}`)); - if (!this.setupDone) { - reject(err); - } else { - this.emit({ type: "error", message: err.message }); - } - }); - - this.ws.on("close", () => { - console.log(dim("[voice] Gemini WS closed")); - }); - - this.ws.on("message", (data) => { - try { - const msg = JSON.parse(data.toString()); - this.handleGeminiMessage(msg); - - // Resolve after setup acknowledgment - if (!this.setupDone && msg.setupComplete) { - this.setupDone = true; - console.log(dim("[voice] Gemini session ready")); - resolve(); - } - } catch (err: any) { - console.error(dim(`[voice] Gemini parse error: ${err.message}`)); - } - }); - }); - } - - send(msg: ClientMessage): void { - switch (msg.type) { - case "audio": - // Browser sends 24kHz, Gemini expects 16kHz - this.sendRaw({ - realtimeInput: { - mediaChunks: [{ - mimeType: "audio/pcm;rate=16000", - data: downsample24kTo16k(msg.audio), - }], - }, - }); - break; - - case "video_frame": - // Gemini supports continuous video streaming natively - this.sendRaw({ - realtimeInput: { - mediaChunks: [{ - mimeType: msg.mimeType, - data: msg.frame, - }], - }, - }); - break; - - case "text": - this.sendRaw({ - clientContent: { - turns: [{ - role: "user", - parts: [{ text: msg.text }], - }], - turnComplete: true, - }, - }); - break; - - case "file": { - const parts: any[] = []; - - if (msg.mimeType.startsWith("image/")) { - parts.push({ inlineData: { mimeType: msg.mimeType, data: msg.data } }); - parts.push({ text: msg.text || `[User attached image: ${msg.name}]` }); - } else { - const decoded = Buffer.from(msg.data, "base64").toString("utf-8"); - const label = msg.text ? `${msg.text}\n\n` : ""; - parts.push({ text: `${label}[File: ${msg.name}]\n\`\`\`\n${decoded}\n\`\`\`` }); - } - - this.sendRaw({ - clientContent: { - turns: [{ role: "user", parts }], - turnComplete: true, - }, - }); - break; - } - } - } - - async disconnect(): Promise { - if (this.ws) { - this.ws.close(); - this.ws = null; - } - } - - private emit(msg: ServerMessage): void { - this.onMessage?.(msg); - } - - private sendSetup(model: string): void { - const instructions = this.config.instructions || DEFAULT_VOICE_INSTRUCTIONS; - - const voiceName = this.config.voice || "Aoede"; - - this.sendRaw({ - setup: { - model, - generationConfig: { - responseModalities: ["AUDIO", "TEXT"], - speechConfig: { - voiceConfig: { - prebuiltVoiceConfig: { voiceName }, - }, - }, - }, - tools: [{ - functionDeclarations: [{ - name: "run_agent", - description: "Execute any request through the gitagent agent. It has full access to the terminal (can run any shell command, open apps, install packages), file system (read/write/create files), git operations, and persistent memory. Use this for ALL actionable requests. IMPORTANT: If the user uploaded a file, always include the file path (from the '[File saved to: ...]' annotation) in the query.", - parameters: { - type: "OBJECT", - properties: { - query: { - type: "STRING", - description: "The user's request. MUST include file paths when referencing uploaded files (e.g. 'make a game using the image at workspace/lobster.png').", - }, - }, - required: ["query"], - }, - }], - }], - systemInstruction: { - parts: [{ text: instructions }], - }, - contextWindowCompression: { - triggerTokens: 25000, - slidingWindow: { targetTokens: 12500 }, - }, - }, - }); - } - - private handleGeminiMessage(msg: any): void { - // Tool calls - if (msg.toolCall) { - this.handleToolCall(msg.toolCall); - return; - } - - // Server content (audio/text responses) - if (msg.serverContent) { - const sc = msg.serverContent; - - // Model turn parts - if (sc.modelTurn?.parts) { - for (const part of sc.modelTurn.parts) { - if (part.inlineData) { - const mimeType: string = part.inlineData.mimeType || ""; - if (mimeType.startsWith("audio/")) { - // Gemini outputs 16kHz, browser expects 24kHz - const audio24k = upsample16kTo24k(part.inlineData.data); - this.emit({ type: "audio_delta", audio: audio24k }); - } - } - if (part.text) { - this.emit({ - type: "transcript", - role: "assistant", - text: part.text, - partial: !sc.turnComplete, - }); - } - } - } - - // Turn complete marker - if (sc.turnComplete && sc.modelTurn?.parts) { - const textParts = sc.modelTurn.parts.filter((p: any) => p.text).map((p: any) => p.text); - if (textParts.length > 0) { - this.emit({ type: "transcript", role: "assistant", text: textParts.join("") }); - } - } - - // Input transcription - if (sc.inputTranscription?.text) { - console.log(dim(`[voice] User: ${sc.inputTranscription.text}`)); - this.emit({ type: "transcript", role: "user", text: sc.inputTranscription.text }); - } - } - } - - private async handleToolCall(toolCall: any): Promise { - if (!this.toolHandler) return; - - const functionCalls = toolCall.functionCalls || []; - const responses: any[] = []; - - for (const fc of functionCalls) { - if (fc.name !== "run_agent") { - console.error(dim(`[voice] Unknown Gemini function call: ${fc.name}`)); - responses.push({ id: fc.id, name: fc.name, response: { error: `Unknown function: ${fc.name}` } }); - continue; - } - - const queryArg = fc.args?.query; - if (!queryArg) { - responses.push({ id: fc.id, name: fc.name, response: { error: "Missing query argument" } }); - continue; - } - - console.log(dim(`[voice] Agent query: ${queryArg}`)); - this.emit({ type: "agent_working", query: queryArg }); - - try { - const result = await this.toolHandler(queryArg); - console.log(dim(`[voice] Agent response: ${result.slice(0, 200)}${result.length > 200 ? "..." : ""}`)); - responses.push({ id: fc.id, name: fc.name, response: { result } }); - this.emit({ type: "agent_done", result: result.slice(0, 500) }); - } catch (err: any) { - console.error(dim(`[voice] Agent error: ${err.message}`)); - responses.push({ id: fc.id, name: fc.name, response: { error: err.message } }); - this.emit({ type: "error", message: err.message }); - } - } - - this.sendRaw({ - toolResponse: { functionResponses: responses }, - }); - } - - private sendRaw(msg: any): void { - if (this.ws && this.ws.readyState === WebSocket.OPEN) { - this.ws.send(JSON.stringify(msg)); - } - } -} diff --git a/src/voice/index.ts b/src/voice/index.ts deleted file mode 100644 index c9abeee..0000000 --- a/src/voice/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export type { - VoiceAdapter, - VoiceAdapterConfig, - VoiceServerOptions, - MultimodalAdapter, - MultimodalAdapterConfig, - AdapterBackend, - ClientMessage, - ServerMessage, -} from "./adapter.js"; -export { OpenAIRealtimeAdapter } from "./openai-realtime.js"; -export { GeminiLiveAdapter } from "./gemini-live.js"; -export { startVoiceServer } from "./server.js"; diff --git a/src/voice/openai-realtime.ts b/src/voice/openai-realtime.ts deleted file mode 100644 index 501fa64..0000000 --- a/src/voice/openai-realtime.ts +++ /dev/null @@ -1,496 +0,0 @@ -import WebSocket from "ws"; -import { - DEFAULT_VOICE_INSTRUCTIONS, - type MultimodalAdapter, - type MultimodalAdapterConfig, - type ClientMessage, - type ServerMessage, -} from "./adapter.js"; - -const dim = (s: string) => `\x1b[2m${s}\x1b[0m`; - -export class OpenAIRealtimeAdapter implements MultimodalAdapter { - private ws: WebSocket | null = null; - private config: MultimodalAdapterConfig; - private latestVideoFrame: { frame: string; mimeType: string } | null = null; - private latestScreenFrame: { frame: string; mimeType: string } | null = null; - private onMessage: ((msg: ServerMessage) => void) | null = null; - private toolHandler: ((query: string) => Promise) | null = null; - private interrupted = false; - - // Session-refresh state - private refreshTimer: NodeJS.Timeout | null = null; - private refreshing = false; - private disposed = false; - // Refresh 5 minutes before OpenAI Realtime's 60-min hard cap - private static readonly REFRESH_AFTER_MS = 55 * 60 * 1000; - - constructor(config: MultimodalAdapterConfig) { - this.config = config; - } - - async connect(opts: { - toolHandler: (query: string) => Promise; - onMessage: (msg: ServerMessage) => void; - }): Promise { - this.onMessage = opts.onMessage; - this.toolHandler = opts.toolHandler; - - const model = this.config.model || "gpt-realtime-2025-08-28"; - const url = `wss://api.openai.com/v1/realtime?model=${model}`; - - // Try direct WebSocket with headers first (native Node.js / real server) - try { - await this.connectWs(url, { - headers: { - Authorization: `Bearer ${this.config.apiKey}`, - }, - }); - return; - } catch (err: any) { - const msg = err?.message || ""; - // Only retry with ephemeral token if auth failed (WebContainer drops headers) - if (!msg.includes("authentication") && !msg.includes("401")) { - throw err; - } - console.log(dim("[voice] Direct auth failed, requesting ephemeral token…")); - } - - // Fallback: get an ephemeral session token via REST (fetch headers work everywhere) - const keyPreview = this.config.apiKey - ? `${this.config.apiKey.slice(0, 7)}...${this.config.apiKey.slice(-4)} (${this.config.apiKey.length} chars)` - : "(empty)"; - console.log(dim(`[voice] API key: ${keyPreview}`)); - const sessionResp = await fetch("https://api.openai.com/v1/realtime/sessions", { - method: "POST", - headers: { - "Authorization": `Bearer ${this.config.apiKey}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ model }), - }); - if (!sessionResp.ok) { - const body = await sessionResp.text(); - throw new Error(`Failed to create realtime session: ${sessionResp.status} ${body}`); - } - const session = await sessionResp.json() as { client_secret?: { value?: string } }; - const ephemeralKey = session.client_secret?.value; - if (!ephemeralKey) { - throw new Error("No ephemeral key returned from realtime sessions endpoint"); - } - - await this.connectWs(url, { - headers: { - Authorization: `Bearer ${ephemeralKey}`, - }, - }); - } - - private connectWs(url: string, opts: any): Promise { - return new Promise((resolve, reject) => { - const ws = new WebSocket(url, opts); - let settled = false; - - ws.on("open", () => { - // Don't resolve yet — wait for first message to confirm auth succeeded. - // Send session.update so the server replies with session.created or error. - this.sendSessionUpdateOn(ws); - }); - - ws.on("error", (err) => { - if (!settled) { - settled = true; - ws.close(); - reject(err); - } else { - console.error(dim(`[voice] WebSocket error: ${err.message}`)); - this.emit({ type: "error", message: err.message }); - } - }); - - ws.on("close", () => { - if (!settled) { - settled = true; - reject(new Error("WebSocket closed before open — authentication likely failed")); - } - console.log(dim("[voice] WebSocket closed")); - }); - - ws.on("message", (data) => { - const event = JSON.parse(data.toString()); - - // Before we've confirmed auth, check for errors - if (!settled) { - if (event.type === "error") { - settled = true; - ws.close(); - const errMsg = event.error?.message || "Unknown auth error"; - reject(new Error(errMsg)); - return; - } - // Any non-error message means auth succeeded - settled = true; - this.ws = ws; - resolve(); - } - - this.handleEvent(event); - }); - }); - } - - /** Send session.update on a specific ws instance (before this.ws is set). */ - private sendSessionUpdateOn(ws: WebSocket): void { - const instructions = this.config.instructions || DEFAULT_VOICE_INSTRUCTIONS; - const payload = { - type: "session.update", - session: { - type: "realtime", - output_modalities: ["audio"], - instructions, - audio: { - input: { - format: { type: "audio/pcm", rate: 24000 }, - turn_detection: { - type: "server_vad", - threshold: 0.6, - prefix_padding_ms: 400, - silence_duration_ms: 800, - create_response: true, - }, - transcription: { model: "whisper-1" }, - }, - output: { - format: { type: "audio/pcm", rate: 24000 }, - voice: this.config.voice || "ash", - }, - }, - tool_choice: "auto", - tools: [ - { - type: "function", - name: "run_agent", - description: "Your ONLY way to take action. This agent runs on the user's Mac with full shell access. It can: run ANY shell command, open apps (open -a Spotify), play music (osascript, afplay, open URLs), browse the web, read/write files, git operations, send emails, manage calendars, install packages, control system settings, and save memories. You MUST call this tool whenever the user asks you to DO anything — play music, open something, check something, build something, send something. NEVER describe an action without calling this tool. If the user asks and you just talk without calling this — you failed.", - parameters: { - type: "object", - properties: { - query: { - type: "string", - description: "What to do. Be specific. Include file paths for uploaded files. Examples: 'Play relaxing music on YouTube using: open https://youtube.com/...', 'Open Spotify and play chill playlist using osascript', 'Save to memory: user likes rock music'", - }, - }, - required: ["query"], - }, - }, - ], - }, - }; - if (ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify(payload)); - } - } - - send(msg: ClientMessage): void { - switch (msg.type) { - case "audio": - this.sendRaw({ - type: "input_audio_buffer.append", - audio: msg.audio, - }); - break; - - case "video_frame": { - // OpenAI doesn't support continuous video. Store latest frame and - // inject it as an image on the next user turn via conversation item. - const source = msg.source || "camera"; - if (source === "screen") { - this.latestScreenFrame = { frame: msg.frame, mimeType: msg.mimeType }; - } else { - this.latestVideoFrame = { frame: msg.frame, mimeType: msg.mimeType }; - } - break; - } - - case "text": { - // Send text as a user conversation item, optionally with latest video frame - const content: any[] = []; - - if (this.latestVideoFrame) { - content.push({ - type: "input_image", - image_url: `data:${this.latestVideoFrame.mimeType};base64,${this.latestVideoFrame.frame}`, - }); - this.latestVideoFrame = null; - } - - content.push({ type: "input_text", text: msg.text }); - - this.sendRaw({ - type: "conversation.item.create", - item: { - type: "message", - role: "user", - content, - }, - }); - this.sendRaw({ type: "response.create" }); - break; - } - - case "file": { - const content: any[] = []; - - if (msg.mimeType.startsWith("image/")) { - content.push({ - type: "input_image", - image_url: `data:${msg.mimeType};base64,${msg.data}`, - }); - content.push({ type: "input_text", text: msg.text || `[User attached image: ${msg.name}]` }); - } else { - const decoded = Buffer.from(msg.data, "base64").toString("utf-8"); - const label = msg.text ? `${msg.text}\n\n` : ""; - content.push({ type: "input_text", text: `${label}[File: ${msg.name}]\n\`\`\`\n${decoded}\n\`\`\`` }); - } - - this.sendRaw({ - type: "conversation.item.create", - item: { type: "message", role: "user", content }, - }); - this.sendRaw({ type: "response.create" }); - break; - } - } - } - - async disconnect(): Promise { - this.disposed = true; - if (this.refreshTimer) { clearTimeout(this.refreshTimer); this.refreshTimer = null; } - if (this.ws) { - this.ws.close(); - this.ws = null; - } - } - - /** - * Tear down and reopen the Realtime WS before (or right after) OpenAI's - * 60-minute hard cap expires. Re-sends the stored session.update so the - * agent picks up where it left off without the user noticing. - */ - private async refreshSession(reason: string): Promise { - if (this.refreshing || this.disposed) return; - this.refreshing = true; - console.log(dim(`[voice] Refreshing Realtime session (${reason})`)); - try { - // Close the old WS without disposing the adapter - if (this.refreshTimer) { clearTimeout(this.refreshTimer); this.refreshTimer = null; } - if (this.ws) { try { this.ws.close(); } catch {} this.ws = null; } - - const model = this.config.model || "gpt-realtime-2025-08-28"; - const url = `wss://api.openai.com/v1/realtime?model=${model}`; - - try { - await this.connectWs(url, { - headers: { - Authorization: `Bearer ${this.config.apiKey}`, - }, - }); - } catch (err: any) { - const msg = err?.message || ""; - if (!msg.includes("authentication") && !msg.includes("401")) throw err; - // Ephemeral token fallback (matches connect() path) - const sessionResp = await fetch("https://api.openai.com/v1/realtime/sessions", { - method: "POST", - headers: { Authorization: `Bearer ${this.config.apiKey}`, "Content-Type": "application/json" }, - body: JSON.stringify({ model }), - }); - if (!sessionResp.ok) throw new Error(`refresh ephemeral token: ${sessionResp.status}`); - const session = (await sessionResp.json()) as { client_secret?: { value?: string } }; - const ephemeralKey = session.client_secret?.value; - if (!ephemeralKey) throw new Error("No ephemeral key on refresh"); - await this.connectWs(url, { - }); - } - console.log(dim("[voice] Session refreshed")); - } catch (err: any) { - console.error(dim(`[voice] Session refresh failed: ${err.message}`)); - this.emit({ type: "error", message: `Voice session refresh failed: ${err.message}` }); - } finally { - this.refreshing = false; - } - } - - private emit(msg: ServerMessage): void { - this.onMessage?.(msg); - } - - /** - * Inject the latest video frame as a conversation item so the model - * can see it when generating the next response (e.g. after a voice turn). - */ - private injectVideoFrame(): void { - // Prefer screen frame over camera — it provides more useful context - const isScreen = !!this.latestScreenFrame; - const frame = this.latestScreenFrame || this.latestVideoFrame; - if (!frame) return; - - // Clear both so we don't inject stale frames - this.latestScreenFrame = null; - this.latestVideoFrame = null; - - console.log(dim(`[voice] Injecting ${isScreen ? "screen" : "camera"} frame into conversation`)); - this.sendRaw({ - type: "conversation.item.create", - item: { - type: "message", - role: "user", - content: [{ - type: "input_image", - image_url: `data:${frame.mimeType};base64,${frame.frame}`, - }], - }, - }); - } - - private sendSessionUpdate(): void { - if (this.ws) this.sendSessionUpdateOn(this.ws); - } - - private handleEvent(event: any): void { - switch (event.type) { - case "session.created": - console.log(dim("[voice] Session created")); - if (this.refreshTimer) clearTimeout(this.refreshTimer); - this.refreshTimer = setTimeout(() => { - this.refreshSession("proactive refresh before 60-min cap").catch(() => {}); - }, OpenAIRealtimeAdapter.REFRESH_AFTER_MS); - break; - - case "session.updated": - console.log(dim("[voice] Session configured")); - break; - - case "input_audio_buffer.speech_started": - // VAD detected start of speech — inject video frame (what user is looking at) - // and cancel any in-progress response so the user can interrupt - this.interrupted = true; - this.injectVideoFrame(); - this.sendRaw({ type: "response.cancel" }); - this.emit({ type: "interrupt" }); - break; - - case "input_audio_buffer.speech_stopped": - break; - - case "conversation.item.input_audio_transcription.completed": - if (event.transcript) { - console.log(dim(`[voice] User: ${event.transcript}`)); - this.emit({ type: "transcript", role: "user", text: event.transcript }); - } - break; - - case "response.created": - // New response starting — accept audio again - this.interrupted = false; - break; - - // GA event names are response.output_audio*; keep the beta aliases too. - case "response.audio.delta": - case "response.output_audio.delta": - if (event.delta && !this.interrupted) { - this.emit({ type: "audio_delta", audio: event.delta }); - } - break; - - case "response.audio_transcript.delta": - case "response.output_audio_transcript.delta": - this.emit({ type: "transcript", role: "assistant", text: event.delta || "", partial: true }); - break; - - case "response.audio_transcript.done": - case "response.output_audio_transcript.done": - if (event.transcript) { - this.emit({ type: "transcript", role: "assistant", text: event.transcript }); - } - break; - - case "response.function_call_arguments.done": - this.handleFunctionCall(event); - break; - - case "error": { - const errMsg = event.error?.message || "Unknown OpenAI error"; - const code = event.error?.code || ""; - console.error(dim(`[voice] Error: ${JSON.stringify(event.error)}`)); - // Don't surface cancellation errors — they happen when user interrupts with no active response - if (errMsg.toLowerCase().includes("cancellation failed")) break; - // Session expired (60-min cap) — silently reconnect instead of surfacing - const lower = errMsg.toLowerCase(); - if ( - lower.includes("maximum duration") || - lower.includes("session_expired") || - code === "session_expired" - ) { - this.refreshSession("session expired").catch(() => {}); - break; - } - this.emit({ type: "error", message: errMsg }); - break; - } - } - } - - private async handleFunctionCall(event: any): Promise { - const callId = event.call_id; - const name = event.name; - - if (name !== "run_agent" || !this.toolHandler) { - console.error(dim(`[voice] Unknown function call: ${name}`)); - return; - } - - let args: { query: string }; - try { - args = JSON.parse(event.arguments); - } catch { - console.error(dim("[voice] Failed to parse function arguments")); - return; - } - - console.log(dim(`[voice] Agent query: ${args.query}`)); - this.emit({ type: "agent_working", query: args.query }); - - try { - const result = await this.toolHandler(args.query); - console.log(dim(`[voice] Agent response: ${result.slice(0, 200)}${result.length > 200 ? "..." : ""}`)); - - this.sendRaw({ - type: "conversation.item.create", - item: { - type: "function_call_output", - call_id: callId, - output: result, - }, - }); - this.sendRaw({ type: "response.create" }); - this.emit({ type: "agent_done", result: result.slice(0, 500) }); - } catch (err: any) { - console.error(dim(`[voice] Agent error: ${err.message}`)); - this.sendRaw({ - type: "conversation.item.create", - item: { - type: "function_call_output", - call_id: callId, - output: `Error: ${err.message}`, - }, - }); - this.sendRaw({ type: "response.create" }); - this.emit({ type: "error", message: err.message }); - } - } - - private sendRaw(event: any): void { - if (this.ws && this.ws.readyState === WebSocket.OPEN) { - this.ws.send(JSON.stringify(event)); - } - } -} diff --git a/src/voice/server.ts b/src/voice/server.ts deleted file mode 100644 index b831b3c..0000000 --- a/src/voice/server.ts +++ /dev/null @@ -1,3265 +0,0 @@ -import { createServer, type Server, type IncomingMessage, type ServerResponse } from "http"; -import { WebSocketServer, WebSocket as WS } from "ws"; -import { query } from "../sdk.js"; -import type { VoiceServerOptions, ClientMessage, ServerMessage, MultimodalAdapter } from "./adapter.js"; -import { readFileSync, readdirSync, statSync, existsSync, writeFileSync, mkdirSync, appendFileSync, rmSync, createReadStream } from "fs"; -import { execSync } from "child_process"; -import { join, dirname, resolve, relative } from "path"; -import { writeFile, readFile, mkdir, stat } from "fs/promises"; -import { fileURLToPath } from "url"; -import { homedir } from "os"; -import { OpenAIRealtimeAdapter } from "./openai-realtime.js"; -import { GeminiLiveAdapter } from "./gemini-live.js"; -import { ComposioAdapter } from "../composio/index.js"; -import type { GCToolDefinition } from "../sdk-types.js"; -import { appendMessage, loadHistory, deleteHistory, summarizeHistory } from "./chat-history.js"; -import { getVoiceContext, getAgentContext } from "../context.js"; -import { discoverSkills } from "../skills.js"; -import { discoverWorkflows, loadFlowDefinition, saveFlowDefinition, deleteFlowDefinition } from "../workflows.js"; -import { discoverSchedules, saveSchedule, deleteSchedule, updateScheduleMeta } from "../schedules.js"; -import { startScheduler, stopScheduler, reloadSchedules, executeScheduledJob } from "../schedule-runner.js"; -import cron from "node-cron"; - -const dim = (s: string) => `\x1b[2m${s}\x1b[0m`; -const bold = (s: string) => `\x1b[1m${s}\x1b[0m`; - -// ── Log ring buffer for Logs UI ─────────────────────────────────────── -interface LogEntry { - id: number; - ts: string; - source: string; - level: "info" | "warn" | "error"; - message: string; -} - -class LogRingBuffer { - private buf: LogEntry[] = []; - private nextId = 1; - private cap: number; - constructor(capacity = 2000) { this.cap = capacity; } - push(source: string, level: "info" | "warn" | "error", message: string): LogEntry { - const entry: LogEntry = { id: this.nextId++, ts: new Date().toISOString(), source, level, message }; - this.buf.push(entry); - if (this.buf.length > this.cap) this.buf.shift(); - return entry; - } - all(): LogEntry[] { return this.buf.slice(); } - since(id: number): LogEntry[] { return this.buf.filter(e => e.id > id); } -} - -const logBuffer = new LogRingBuffer(2000); -let logBroadcast: ((entry: LogEntry) => void) | null = null; - -function stripAnsi(s: string): string { return s.replace(/\x1b\[\d*m/g, ""); } -function extractSource(msg: string): { source: string; cleaned: string } { - const m = msg.match(/^\[(\w+(?:\/\w+)?)\]\s*/); - if (m) return { source: m[1].split("/")[0].toLowerCase(), cleaned: msg.slice(m[0].length) }; - return { source: "system", cleaned: msg }; -} - -function formatArg(a: any): string { - if (a == null) return String(a); - if (typeof a === "string") return a; - if (a instanceof Error) return `${a.message}${a.stack ? "\n" + a.stack : ""}`; - try { return JSON.stringify(a, (_k, v) => v instanceof Error ? { message: v.message, stack: v.stack } : v); } - catch { return String(a); } -} - -export function logToBuffer(source: string, level: "info" | "warn" | "error", message: string): LogEntry { - const entry = logBuffer.push(source, level, message); - if (logBroadcast) logBroadcast(entry); - return entry; -} - -function installConsoleIntercept() { - const origLog = console.log.bind(console); - const origError = console.error.bind(console); - const origWarn = console.warn.bind(console); - - function intercept(level: "info" | "warn" | "error", origFn: (...args: any[]) => void, ...args: any[]) { - origFn(...args); - try { - const raw = args.map(formatArg).join(" "); - const clean = stripAnsi(raw); - if (!clean.trim()) return; - const { source, cleaned } = extractSource(clean); - const entry = logBuffer.push(source, level, cleaned); - if (logBroadcast) logBroadcast(entry); - } catch { /* non-fatal */ } - } - - console.log = (...args: any[]) => intercept("info", origLog, ...args); - console.error = (...args: any[]) => intercept("error", origError, ...args); - console.warn = (...args: any[]) => intercept("warn", origWarn, ...args); -} - -installConsoleIntercept(); - -// Global error handlers — capture everything that would otherwise be lost -if (!(process as any).__gitagentLogHandlersInstalled) { - (process as any).__gitagentLogHandlersInstalled = true; - process.on("uncaughtException", (err: Error) => { - console.error(`[system] UNCAUGHT EXCEPTION: ${err.message}\n${err.stack}`); - }); - process.on("unhandledRejection", (reason: any) => { - const msg = reason instanceof Error ? `${reason.message}\n${reason.stack}` : String(reason); - console.error(`[system] UNHANDLED REJECTION: ${msg}`); - }); - process.on("warning", (warning: Error) => { - console.warn(`[system] Node warning: ${warning.name}: ${warning.message}`); - }); -} - -// ── File type / MIME helper ──────────────────────────────────────────── -export type FileKind = "html" | "image" | "pdf" | "video" | "audio" | "markdown" | "text" | "binary"; -export interface FileTypeInfo { mime: string; kind: FileKind; } - -const FILE_TYPES: Record = { - // html - html: { mime: "text/html; charset=utf-8", kind: "html" }, - htm: { mime: "text/html; charset=utf-8", kind: "html" }, - // images - png: { mime: "image/png", kind: "image" }, - jpg: { mime: "image/jpeg", kind: "image" }, - jpeg: { mime: "image/jpeg", kind: "image" }, - gif: { mime: "image/gif", kind: "image" }, - webp: { mime: "image/webp", kind: "image" }, - svg: { mime: "image/svg+xml", kind: "image" }, - bmp: { mime: "image/bmp", kind: "image" }, - ico: { mime: "image/x-icon", kind: "image" }, - avif: { mime: "image/avif", kind: "image" }, - // pdf - pdf: { mime: "application/pdf", kind: "pdf" }, - // video - mp4: { mime: "video/mp4", kind: "video" }, - webm: { mime: "video/webm", kind: "video" }, - mov: { mime: "video/quicktime", kind: "video" }, - m4v: { mime: "video/x-m4v", kind: "video" }, - // audio - mp3: { mime: "audio/mpeg", kind: "audio" }, - wav: { mime: "audio/wav", kind: "audio" }, - ogg: { mime: "audio/ogg", kind: "audio" }, - m4a: { mime: "audio/mp4", kind: "audio" }, - aac: { mime: "audio/aac", kind: "audio" }, - flac: { mime: "audio/flac", kind: "audio" }, - // markdown - md: { mime: "text/markdown; charset=utf-8", kind: "markdown" }, - markdown: { mime: "text/markdown; charset=utf-8", kind: "markdown" }, - // text-ish - txt: { mime: "text/plain; charset=utf-8", kind: "text" }, - json: { mime: "application/json; charset=utf-8", kind: "text" }, - js: { mime: "text/javascript; charset=utf-8", kind: "text" }, - mjs: { mime: "text/javascript; charset=utf-8", kind: "text" }, - cjs: { mime: "text/javascript; charset=utf-8", kind: "text" }, - ts: { mime: "text/plain; charset=utf-8", kind: "text" }, - tsx: { mime: "text/plain; charset=utf-8", kind: "text" }, - jsx: { mime: "text/plain; charset=utf-8", kind: "text" }, - css: { mime: "text/css; charset=utf-8", kind: "text" }, - yaml: { mime: "text/yaml; charset=utf-8", kind: "text" }, - yml: { mime: "text/yaml; charset=utf-8", kind: "text" }, - toml: { mime: "text/plain; charset=utf-8", kind: "text" }, - csv: { mime: "text/csv; charset=utf-8", kind: "text" }, - log: { mime: "text/plain; charset=utf-8", kind: "text" }, - sh: { mime: "text/x-shellscript; charset=utf-8", kind: "text" }, - py: { mime: "text/x-python; charset=utf-8", kind: "text" }, - go: { mime: "text/plain; charset=utf-8", kind: "text" }, - rs: { mime: "text/plain; charset=utf-8", kind: "text" }, - java: { mime: "text/x-java; charset=utf-8", kind: "text" }, - c: { mime: "text/x-c; charset=utf-8", kind: "text" }, - cpp: { mime: "text/x-c++; charset=utf-8", kind: "text" }, - h: { mime: "text/x-c; charset=utf-8", kind: "text" }, - xml: { mime: "application/xml; charset=utf-8", kind: "text" }, - // office / archives — kind: binary, but with proper MIME so downloads name correctly - doc: { mime: "application/msword", kind: "binary" }, - docx: { mime: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", kind: "binary" }, - xls: { mime: "application/vnd.ms-excel", kind: "binary" }, - xlsx: { mime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", kind: "binary" }, - ppt: { mime: "application/vnd.ms-powerpoint", kind: "binary" }, - pptx: { mime: "application/vnd.openxmlformats-officedocument.presentationml.presentation", kind: "binary" }, - zip: { mime: "application/zip", kind: "binary" }, - tar: { mime: "application/x-tar", kind: "binary" }, - gz: { mime: "application/gzip", kind: "binary" }, -}; - -export function fileTypeFor(pathOrName: string): FileTypeInfo { - const name = pathOrName.split("/").pop() || pathOrName; - const dot = name.lastIndexOf("."); - const ext = dot >= 0 ? name.slice(dot + 1).toLowerCase() : ""; - return FILE_TYPES[ext] || { mime: "application/octet-stream", kind: "binary" }; -} - -const MAX_FILE_BYTES = (() => { - const v = parseInt(process.env.GITAGENT_MAX_FILE_BYTES || "", 10); - return Number.isFinite(v) && v > 0 ? v : 200 * 1024 * 1024; -})(); - -export const CLOUD_MODE = - process.env.GITAGENT_CLOUD === "true" || - !!process.env.KUBERNETES_SERVICE_HOST || - !!process.env.RENDER || - !!process.env.FLY_APP_NAME; - -const CLOUD_VOICE_SUFFIX = - " CLOUD MODE: You are running inside a containerized cloud deployment — there is no desktop, no `open`/`xdg-open`/`osascript`, " + - "no Spotify, no Apple Music, no GUI apps. Do NOT instruct run_agent to call those. " + - "To 'show' the user something, write the artifact to `workspace/` (e.g. `workspace/index.html`, `workspace/deck.pptx`) " + - "and mention the path in your reply — the web UI auto-opens it (HTML renders inline, PDFs preview, Office files offer Download)."; - -function streamFileWithRange( - req: IncomingMessage, - res: ServerResponse, - abs: string, - opts: { mime: string; download?: boolean; filename?: string; extraHeaders?: Record }, -): void { - const st = statSync(abs); - if (st.size > MAX_FILE_BYTES) { - res.writeHead(413, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: `File too large (>${Math.floor(MAX_FILE_BYTES / 1024 / 1024)}MB)` })); - return; - } - const headers: Record = { - "Content-Type": opts.mime, - "Cache-Control": "no-cache", - "Accept-Ranges": "bytes", - ...(opts.extraHeaders || {}), - }; - if (opts.download) { - const fn = (opts.filename || abs.split("/").pop() || "download").replace(/"/g, ""); - headers["Content-Disposition"] = `attachment; filename="${fn}"`; - } - const range = req.headers.range; - if (range) { - const m = /^bytes=(\d*)-(\d*)$/.exec(range); - if (m) { - const start = m[1] ? parseInt(m[1], 10) : 0; - const end = m[2] ? parseInt(m[2], 10) : st.size - 1; - if (Number.isFinite(start) && Number.isFinite(end) && start <= end && start < st.size) { - const length = end - start + 1; - headers["Content-Range"] = `bytes ${start}-${end}/${st.size}`; - headers["Content-Length"] = String(length); - res.writeHead(206, headers); - createReadStream(abs, { start, end }).pipe(res); - return; - } - } - } - headers["Content-Length"] = String(st.size); - res.writeHead(200, headers); - createReadStream(abs).pipe(res); -} - -// ── Background memory saver ──────────────────────────────────────────── -// Patterns that indicate the user is sharing personal info worth saving. -// This runs server-side so we don't depend on the voice LLM deciding to save. -const MEMORY_PATTERNS = [ - /\bi (?:like|love|enjoy|prefer|hate|dislike)\b/i, - /\bmy (?:name|dog|cat|favorite|fav|hobby|job|car|team)\b/i, - /\bi(?:'m| am) (?:a |into |from |working on )/i, - /\bi(?:'m| am) \w+$/i, // "I am Shreyas", "I'm Zeus" - /\bmy name is\b/i, // "my name is ..." - /\bcall me\b/i, - /\bremember (?:that|this)\b/i, - /\bi (?:play|watch|drive|use|work with|listen to)\b/i, - /\bi(?:'m| am) \d+/i, // "I'm 25", age - /\bi (?:live|grew up|was born) (?:in|at|near)\b/i, // location info - /\bpeople call me\b/i, -]; - -function isMemoryWorthy(text: string): boolean { - return MEMORY_PATTERNS.some((p) => p.test(text)); -} - -// ── Moment detection for photo capture ───────────────────────────────── -const MOMENT_PATTERNS = [ - /\bhaha\b/i, - /\blol\b/i, - /\blmao\b/i, - /\blove it\b/i, - /\bthat'?s amazing\b/i, - /\bso happy\b/i, - /\bbest day\b/i, - /\bwe did it\b/i, - /\bnailed it\b/i, - /\blet'?s go\b/i, - /\bhell yeah\b/i, - /\bawesome\b/i, - /\bthank you so much\b/i, - /\bfirst time\b/i, - /\bmilestone\b/i, - /\bcelebrat/i, - /\bincredible\b/i, -]; - -function isMomentWorthy(text: string): boolean { - return MOMENT_PATTERNS.some((p) => p.test(text)); -} - -let vitalsTokenCount = 0; - -// ── Centralized vitals snapshot ──────────────────────────────────────── -// All surfaces (API, UI, logs) share the same cached snapshot so values -// are always consistent regardless of who reads them or when. -interface VitalsSnapshot { - cpu: number; - mem: number; - heapUsed: number; - heapTotal: number; - uptime: number; - tokens: number; - ts: number; // unix-ms when this snapshot was taken -} - -let _lastCpuUsage = process.cpuUsage(); -let _lastCpuTime = process.hrtime.bigint(); -let _vitalsCache: VitalsSnapshot | null = null; -const VITALS_CACHE_MS = 1000; // cache for 1s — all readers within 1s see identical values - -function getVitalsSnapshot(): VitalsSnapshot { - const now = Date.now(); - if (_vitalsCache && now - _vitalsCache.ts < VITALS_CACHE_MS) return _vitalsCache; - - const mem = process.memoryUsage(); - const currentCpu = process.cpuUsage(); - const currentTime = process.hrtime.bigint(); - - // Delta-based CPU: measure CPU microseconds consumed since last sample - const userDelta = currentCpu.user - _lastCpuUsage.user; - const sysDelta = currentCpu.system - _lastCpuUsage.system; - const wallDeltaUs = Number(currentTime - _lastCpuTime) / 1000; // ns → µs - const cpuPercent = wallDeltaUs > 0 - ? Math.min(100, Math.round((userDelta + sysDelta) / wallDeltaUs * 100)) - : 0; - - _lastCpuUsage = currentCpu; - _lastCpuTime = currentTime; - - _vitalsCache = { - cpu: cpuPercent, - mem: Math.round(mem.rss / 1024 / 1024), - heapUsed: Math.round(mem.heapUsed / 1024 / 1024), - heapTotal: Math.round(mem.heapTotal / 1024 / 1024), - uptime: Math.round(process.uptime()), - tokens: vitalsTokenCount, - ts: now, - }; - return _vitalsCache; -} -const PHOTOS_DIR = "memory/photos"; -const INDEX_FILE = "memory/photos/INDEX.md"; -const LATEST_FRAME_FILE = "memory/.latest-frame.jpg"; -const LATEST_SCREEN_FILE = "memory/.latest-screen.jpg"; - -function slugify(text: string): string { - return text - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-|-$/g, "") - .slice(0, 40); -} - -// ── Mood tracking ────────────────────────────────────────────────────── -type Mood = "happy" | "frustrated" | "curious" | "excited" | "calm"; -const MOOD_SIGNALS: { mood: Mood; patterns: RegExp[] }[] = [ - { mood: "happy", patterns: [/\bhaha\b/i, /\blol\b/i, /\blove it\b/i, /\bthat'?s great\b/i, /\bnice\b/i, /\bawesome\b/i, /\bamazing\b/i] }, - { mood: "frustrated", patterns: [/\bugh\b/i, /\bwhat the\b/i, /\bdamn\b/i, /\bstill broken\b/i, /\bnot working\b/i, /\bwhy (?:is|does|won'?t)\b/i, /\bfuck\b/i] }, - { mood: "curious", patterns: [/\bhow (?:do|does|can|would)\b/i, /\bwhat (?:is|are|if)\b/i, /\bwhy (?:do|does|is)\b/i, /\bexplain\b/i, /\btell me about\b/i] }, - { mood: "excited", patterns: [/\blet'?s go\b/i, /\bhell yeah\b/i, /\bwe did it\b/i, /\bnailed it\b/i, /\byes!\b/i, /\bfinally\b/i] }, - { mood: "calm", patterns: [/\bokay\b/i, /\bsure\b/i, /\bcool\b/i, /\bsounds good\b/i, /\bgot it\b/i] }, -]; - -function detectMood(text: string): Mood | null { - for (const { mood, patterns } of MOOD_SIGNALS) { - if (patterns.some((p) => p.test(text))) return mood; - } - return null; -} - -interface MoodCounts { happy: number; frustrated: number; curious: number; excited: number; calm: number } - -function dominantMood(counts: MoodCounts): Mood { - let best: Mood = "calm"; - let max = 0; - for (const [mood, count] of Object.entries(counts) as [Mood, number][]) { - if (count > max) { max = count; best = mood; } - } - return best; -} - -async function saveMoodEntry(agentDir: string, counts: MoodCounts, messageCount: number): Promise { - if (messageCount < 3) return; // Skip trivially short sessions - - const now = new Date(); - const pad = (n: number) => String(n).padStart(2, "0"); - const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`; - const time = `${pad(now.getHours())}:${pad(now.getMinutes())}`; - const mood = dominantMood(counts); - - const moodPath = join(agentDir, "memory", "mood.md"); - let existing = ""; - try { existing = await readFile(moodPath, "utf-8"); } catch { - existing = "# Mood Log\n\n"; - } - - const detail = Object.entries(counts).filter(([, v]) => v > 0).map(([k, v]) => `${k}:${v}`).join(" "); - existing += `- ${date} ${time} — **${mood}** (${detail}) [${messageCount} msgs]\n`; - - await mkdir(join(agentDir, "memory"), { recursive: true }); - await writeFile(moodPath, existing, "utf-8"); - - try { - execSync(`git add "memory/mood.md" && git commit -m "Mood: ${mood} session (${date} ${time})"`, { - cwd: agentDir, stdio: "pipe", - }); - } catch { /* file saved even if commit fails */ } -} - -// ── Session journaling ───────────────────────────────────────────────── -async function writeJournalEntry( - agentDir: string, - branch: string, - moodCounts: MoodCounts, - model?: string, - env?: string, -): Promise { - const messages = loadHistory(agentDir, branch); - if (messages.length < 5) return; - - const lines: string[] = []; - for (const msg of messages.slice(-50)) { - if (msg.type === "transcript") lines.push(`${msg.role}: ${msg.text}`); - else if (msg.type === "agent_done") lines.push(`agent: ${msg.result.slice(0, 200)}`); - } - if (lines.length < 3) return; - - let transcript = lines.join("\n"); - if (transcript.length > 3000) transcript = transcript.slice(-3000); - - const mood = dominantMood(moodCounts); - const prompt = `Write a brief journal entry (3-5 sentences) reflecting on this conversation session. Mood was mostly: ${mood}. Note what was accomplished, any unfinished threads, and how the user seemed. Write in first person as the agent. Be genuine, not corporate.\n\nTranscript:\n${transcript}`; - - try { - const result = query({ - prompt, - dir: agentDir, - model, - env, - maxTurns: 1, - replaceBuiltinTools: true, - tools: [], - systemPrompt: "You are journaling about your day as an AI assistant. Write naturally and briefly.", - }); - - let entry = ""; - for await (const msg of result) { - if (msg.type === "assistant" && msg.content) entry += msg.content; - } - entry = entry.trim(); - if (!entry) return; - - const now = new Date(); - const pad = (n: number) => String(n).padStart(2, "0"); - const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`; - const time = `${pad(now.getHours())}:${pad(now.getMinutes())}`; - - const journalDir = join(agentDir, "memory", "journal"); - await mkdir(journalDir, { recursive: true }); - const journalPath = join(journalDir, `${date}.md`); - - let existing = ""; - try { existing = await readFile(journalPath, "utf-8"); } catch { - existing = `# Journal — ${date}\n\n`; - } - existing += `### ${time} (${mood})\n${entry}\n\n`; - await writeFile(journalPath, existing, "utf-8"); - - try { - execSync(`git add "memory/journal/${date}.md" && git commit -m "Journal: ${date} ${time} session reflection"`, { - cwd: agentDir, stdio: "pipe", - }); - console.error(dim(`[voice] Journal entry written for ${date} ${time}`)); - } catch { /* saved even if commit fails */ } - } catch (err: any) { - console.error(dim(`[voice] Journal write failed: ${err.message}`)); - } -} - -async function capturePhoto( - agentDir: string, - reason: string, - frameData?: Buffer, -): Promise { - // If no frame passed directly, read from temp file - let frame = frameData; - if (!frame) { - const framePath = join(agentDir, LATEST_FRAME_FILE); - try { - const frameStat = await stat(framePath); - if (Date.now() - frameStat.mtimeMs > 5000) { - console.error(dim("[voice] No recent camera frame, skipping photo capture")); - return; - } - frame = await readFile(framePath); - } catch { - console.error(dim("[voice] No camera frame available, skipping photo capture")); - return; - } - } - - const now = new Date(); - const pad = (n: number) => String(n).padStart(2, "0"); - const datePart = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`; - const timePart = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`; - const slug = slugify(reason); - const filename = `${datePart}_${timePart}_${slug}.jpg`; - const photoRelPath = `${PHOTOS_DIR}/${filename}`; - const photoAbsPath = join(agentDir, photoRelPath); - - await mkdir(join(agentDir, PHOTOS_DIR), { recursive: true }); - await writeFile(photoAbsPath, frame); - - // Update INDEX.md - const indexPath = join(agentDir, INDEX_FILE); - let indexContent = ""; - try { - indexContent = await readFile(indexPath, "utf-8"); - } catch { - indexContent = "# Memorable Moments\n\nPhotos captured during happy and memorable moments.\n\n"; - } - const entry = `- **${datePart} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}** — ${reason} → [\`${filename}\`](${filename})\n`; - indexContent += entry; - await writeFile(indexPath, indexContent, "utf-8"); - - // Git add + commit - const commitMsg = `Capture moment: ${reason}`; - try { - execSync(`git add "${photoRelPath}" "${INDEX_FILE}" && git commit -m "${commitMsg.replace(/"/g, '\\"')}"`, { - cwd: agentDir, - stdio: "pipe", - }); - console.error(dim(`[voice] Photo captured: ${filename}`)); - } catch (err: any) { - console.error(dim(`[voice] Photo saved but git commit failed: ${err.stderr?.toString().trim() || "unknown"}`)); - } -} - -function saveMemoryInBackground( - text: string, - agentDir: string, - model?: string, - env?: string, - onStart?: () => void, - onComplete?: () => void, -): void { - const prompt = `The user just said: "${text}"\n\nSave any personal information, preferences, or facts about the user to memory. Use the memory tool to write or update a memory file. Use a descriptive commit message like "Remember: user likes mustangs" or "Save preference: favorite game is GTA 5". Be concise. If there's nothing meaningful to save, do nothing.`; - console.error(dim(`[voice] Background memory save triggered for: "${text.slice(0, 60)}..."`)); - - if (onStart) onStart(); - - // Fire and forget — don't block the voice conversation - (async () => { - try { - const result = query({ - prompt, - dir: agentDir, - model, - env, - maxTurns: 3, - }); - // Drain the iterator to completion - for await (const msg of result) { - if (msg.type === "tool_use") { - console.error(dim(`[voice/memory] Tool: ${msg.toolName}`)); - } - } - console.error(dim("[voice/memory] Background save complete")); - if (onComplete) onComplete(); - } catch (err: any) { - console.error(dim(`[voice/memory] Background save failed: ${err.message}`)); - if (onComplete) onComplete(); - } - })(); -} - -/** Load .env file into process.env (won't overwrite existing vars) */ -function loadEnvFile(dir: string) { - const envPath = join(dir, ".env"); - try { - const content = readFileSync(envPath, "utf-8"); - for (const line of content.split("\n")) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith("#")) continue; - const eq = trimmed.indexOf("="); - if (eq < 1) continue; - const key = trimmed.slice(0, eq).trim(); - let val = trimmed.slice(eq + 1).trim(); - // Strip surrounding quotes - if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { - val = val.slice(1, -1); - } - // Agent .env takes precedence over inherited env (e.g. shell placeholders - // like a stray OPENAI_API_KEY="your-...-here" in ~/.zshrc). - process.env[key] = val; - } - } catch { - // No .env file — that's fine - } -} - -function createAdapter(opts: VoiceServerOptions): MultimodalAdapter { - switch (opts.adapter) { - case "openai-realtime": - return new OpenAIRealtimeAdapter(opts.adapterConfig); - case "gemini-live": - return new GeminiLiveAdapter(opts.adapterConfig); - default: - throw new Error(`Unknown adapter: ${opts.adapter}`); - } -} - -function loadUIHtml(): string { - // Try dist/voice/ui.html first (built), then src/voice/ui.html (dev) - const thisDir = dirname(fileURLToPath(import.meta.url)); - const candidates = [ - join(thisDir, "ui.html"), - join(thisDir, "..", "..", "src", "voice", "ui.html"), - ]; - for (const path of candidates) { - try { - return readFileSync(path, "utf-8"); - } catch { - // try next - } - } - return "

UI not found

Run: npm run build

"; -} - -export async function startVoiceServer(opts: VoiceServerOptions): Promise<() => Promise> { - // Env precedence (lowest → highest): inherited env → ~/.gitagent/.env (global fallback) → agent-dir .env (winner). - // Each loader call overrides whatever's already in process.env, so the LAST load wins. - loadEnvFile(join(homedir(), ".gitagent")); - loadEnvFile(resolve(opts.agentDir)); - - const port = opts.port || 3333; - let agentName = "GitAgent"; - try { - const yamlRaw = readFileSync(join(resolve(opts.agentDir), "agent.yaml"), "utf-8"); - const m = yamlRaw.match(/^name:\s*(.+)$/m); - if (m) agentName = m[1].trim(); - } catch { /* fallback to default */ } - // Re-read on every request so `npm run build` is picked up live without a server restart. - // The file sits in the OS page cache, so the per-request cost is negligible. - function buildUiHtml(): string { - return loadUIHtml() - .replace(/\{\{AGENT_NAME\}\}/g, agentName) - .replace(/\{\{HAS_COMPOSIO\}\}/g, process.env.COMPOSIO_API_KEY ? "true" : "false"); - } - - // Current date/time context injected into every query - function getCurrentDateTimeContext(): string { - const now = new Date(); - const day = now.toLocaleDateString("en-US", { weekday: "long" }); - const date = now.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" }); - const time = now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: true }); - return `Current date and time: ${day}, ${date}, ${time}.`; - } - - // Shared helper: fetch Composio tools + build prompt suffix for any channel - async function getComposioContext(prompt: string): Promise<{ tools: GCToolDefinition[]; promptSuffix: string | undefined }> { - let composioTools: GCToolDefinition[] = []; - let connectedSlugs: string[] = []; - if (composioAdapter) { - try { - connectedSlugs = await composioAdapter.getConnectedToolkitSlugs(); - console.error(`[voice] Connected toolkit slugs: [${connectedSlugs.join(", ")}]`); - if (connectedSlugs.length > 0) { - composioTools = await composioAdapter.getToolsForQuery(prompt); - console.error(`[voice] Semantic search returned ${composioTools.length} tools`); - if (composioTools.length === 0) { - const allTools = await composioAdapter.getTools(); - composioTools = allTools.slice(0, 15); - console.error(`[voice] Fallback capped to ${composioTools.length}/${allTools.length} tools`); - } - console.error(`[voice] Composio: ${composioTools.length} tools: ${composioTools.map(t => t.name).join(", ")}`); - } else { - console.error(`[voice] No connected toolkits found for user`); - } - } catch (err: any) { - console.error(`[voice] Composio tool fetch FAILED: ${err.message}\n${err.stack}`); - } - } else { - console.error(`[voice] composioAdapter is NULL — COMPOSIO_API_KEY not set?`); - } - - let promptSuffix: string | undefined; - if (composioAdapter) { - const parts = [ - `You have access to external services via Composio integration (Gmail, Google Calendar, GitHub, Slack, and many more).`, - `You CAN perform real actions — send emails, read emails, check calendars, create events, manage repos, etc.`, - `NEVER tell the user you "can't access" or "don't have access to" external services. Always attempt to use the available Composio tools (prefixed "composio_") first.`, - `When the user asks to send an email, use the composio SEND_EMAIL tool directly — do NOT create a draft unless they explicitly ask for a draft.`, - `When the user asks about their calendar, use the composio calendar tools to fetch real events.`, - `Prefer Composio tools over CLI commands for any external service interaction.`, - ]; - if (connectedSlugs.length > 0) { - const services = connectedSlugs.map((s) => s.replace(/_/g, " ")).join(", "); - parts.unshift(`Currently connected services: ${services}.`); - } - promptSuffix = parts.join(" "); - } - - return { tools: composioTools, promptSuffix }; - } - - // Creates a per-connection tool handler that can stream events to the browser - function createToolHandler(sendToBrowser: (msg: ServerMessage) => void) { - return async (prompt: string): Promise => { - const { tools: composioTools, promptSuffix: composioPromptSuffix } = await getComposioContext(prompt); - - let systemPromptSuffix = getCurrentDateTimeContext(); - systemPromptSuffix += "\nWhen creating files (PDFs, images, documents, markdown files, code output, etc.), write them to the workspace/ directory by default. If the user explicitly specifies a different path, use the path they requested."; - if (whatsappSock && whatsappConnected) { - systemPromptSuffix += "\nYou can send WhatsApp messages using the send_whatsapp_message tool and set up auto-response triggers using create_trigger."; - } else { - systemPromptSuffix += "\nYou can set up auto-response triggers using create_trigger for when messaging platforms are connected."; - } - if (composioPromptSuffix) systemPromptSuffix += "\n\n" + composioPromptSuffix; - - // Inject shared context (memory + conversation summary) - const agentContext = await getAgentContext(opts.agentDir, activeBranch); - if (agentContext) { - systemPromptSuffix = (systemPromptSuffix || "") + "\n\n" + agentContext; - } - - const uiTools: GCToolDefinition[] = [ - ...createTriggerTools(opts.agentDir), - ...(whatsappSock && whatsappConnected ? createWhatsAppTools(whatsappSock, opts.agentDir) : []), - ...composioTools, - ]; - const result = query({ - prompt, - dir: opts.agentDir, - model: opts.model, - env: opts.env, - ...(uiTools.length ? { tools: uiTools } : {}), - ...(systemPromptSuffix ? { systemPromptSuffix } : {}), - }); - - let text = ""; - const toolResults: string[] = []; - const errors: string[] = []; - - for await (const msg of result) { - if (msg.type === "assistant" && msg.content) { - text += msg.content; - vitalsTokenCount += Math.ceil(msg.content.length / 4); - } else if (msg.type === "tool_use") { - sendToBrowser({ type: "tool_call", toolName: msg.toolName, args: msg.args }); - console.log(dim(`[voice] Tool call: ${msg.toolName}(${JSON.stringify(msg.args).slice(0, 80)})`)); - } else if (msg.type === "tool_result") { - sendToBrowser({ type: "tool_result", toolName: msg.toolName, content: msg.content, isError: msg.isError }); - if (msg.content) { toolResults.push(msg.content); vitalsTokenCount += Math.ceil(msg.content.length / 4); } - console.log(dim(`[voice] Tool ${msg.toolName}: ${msg.content.slice(0, 100)}${msg.content.length > 100 ? "..." : ""}`)); - } else if (msg.type === "system" && msg.subtype === "error") { - errors.push(msg.content); - console.error(dim(`[voice] Agent error: ${msg.content}`)); - } else if (msg.type === "delta" && msg.deltaType === "thinking") { - sendToBrowser({ type: "agent_thinking", text: msg.content }); - } - } - - if (text) return text; - if (errors.length > 0) return `Error: ${errors.join("; ")}`; - if (toolResults.length > 0) return toolResults.join("\n"); - return "(no response)"; - }; - } - - // ── SkillFlow execution ───────────────────────────────────────────── - // ── Approval gate state ──────────────────────────────────────────── - let pendingApproval: { resolve: (approved: boolean) => void } | null = null; - - function handleApprovalReply(text: string): boolean { - if (!pendingApproval) return false; - const lower = text.trim().toLowerCase(); - if (["yes", "approve", "continue", "ok", "go", "y", "proceed"].includes(lower)) { - pendingApproval.resolve(true); - pendingApproval = null; - return true; - } - if (["no", "deny", "stop", "cancel", "abort", "n", "reject"].includes(lower)) { - pendingApproval.resolve(false); - pendingApproval = null; - return true; - } - return false; - } - - async function sendApprovalRequest(channel: string, message: string): Promise { - // Send message via the chosen channel - if (channel === "telegram" && telegramToken && lastTelegramChatId) { - await fetch(`https://api.telegram.org/bot${telegramToken}/sendMessage`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ chat_id: lastTelegramChatId, text: message }), - }); - } else if (channel === "whatsapp" && whatsappSock && whatsappConnected && lastWhatsAppJid) { - const sent = await whatsappSock.sendMessage(lastWhatsAppJid, { text: message }); - if (sent?.key?.id) whatsappSentIds.add(sent.key.id); - } else { - return true; // No channel available — auto-approve - } - - // Wait for reply (timeout after 5 minutes) - return new Promise((resolve) => { - pendingApproval = { resolve }; - const timeout = setTimeout(() => { - if (pendingApproval?.resolve === resolve) { - pendingApproval = null; - resolve(false); // Timeout = deny - } - }, 5 * 60 * 1000); - const origResolve = resolve; - pendingApproval.resolve = (val: boolean) => { - clearTimeout(timeout); - origResolve(val); - }; - }); - } - - async function executeFlow(flowName: string, userContext: string, sendToBrowser: (msg: ServerMessage) => void) { - const flowPath = join(resolve(opts.agentDir), "workflows", flowName + ".yaml"); - const flow = await loadFlowDefinition(flowPath); - - sendToBrowser({ type: "transcript", role: "assistant", - text: `Running flow: ${flow.name} (${flow.steps.length} steps)` }); - - let runningContext = userContext; - - for (let i = 0; i < flow.steps.length; i++) { - const step = flow.steps[i]; - - // ── Approval gate step ── - if (step.skill === "__approval_gate__") { - const channel = step.channel || "telegram"; - const customMsg = step.prompt || ""; - const approvalMsg = customMsg - ? `⏸ Approval Required: ${customMsg}\n\nReply YES to continue or NO to cancel.` - : `⏸ Flow "${flow.name}" paused at step ${i + 1}/${flow.steps.length}.\n\nCompleted so far:\n${runningContext.slice(0, 500)}\n\nReply YES to continue or NO to cancel.`; - - sendToBrowser({ type: "transcript", role: "assistant", - text: `⏸ Waiting for approval via ${channel}...` }); - - const approved = await sendApprovalRequest(channel, approvalMsg); - - if (!approved) { - sendToBrowser({ type: "transcript", role: "assistant", - text: `Flow "${flow.name}" was denied at approval gate (step ${i + 1}).` }); - return; - } - sendToBrowser({ type: "transcript", role: "assistant", - text: `✓ Approval received — continuing flow.` }); - runningContext += `\n\n[Step ${i + 1}: approval gate]: Approved via ${channel}`; - continue; - } - - sendToBrowser({ type: "agent_working" as any, query: `Step ${i + 1}/${flow.steps.length}: ${step.skill}` } as any); - - const prompt = `Use the skill "${step.skill}" (load it with /skill:${step.skill}). -${step.prompt.replace(/\{input\}/g, userContext)} - -Context from previous steps: -${runningContext}`; - - const result = query({ - prompt, - dir: opts.agentDir, - model: opts.model, - env: opts.env, - }); - - let stepOutput = ""; - for await (const msg of result) { - if (msg.type === "assistant" && msg.content) stepOutput += msg.content; - if (msg.type === "tool_use") sendToBrowser({ type: "tool_call", toolName: msg.toolName, args: msg.args } as any); - if (msg.type === "tool_result") sendToBrowser({ type: "tool_result", toolName: msg.toolName, content: msg.content, isError: msg.isError } as any); - } - - runningContext += `\n\n[Step ${i + 1} result (${step.skill})]: ${stepOutput}`; - sendToBrowser({ type: "agent_done" as any, result: `Step ${i + 1} complete` } as any); - } - - sendToBrowser({ type: "transcript", role: "assistant", text: `Flow "${flow.name}" completed.` }); - } - - // ── File API helpers ──────────────────────────────────────────────── - const HIDDEN_DIRS = new Set([".git", "node_modules", ".gitagent", "dist", ".next", "__pycache__", ".venv"]); - const agentRoot = resolve(opts.agentDir); - let activeBranch = execSync("git rev-parse --abbrev-ref HEAD", { cwd: agentRoot, encoding: "utf-8" }).trim(); - const pendingShutdownWork: Promise[] = []; - - // ── Composio integration (optional) ──────────────────────────────── - let composioAdapter: ComposioAdapter | null = null; - if (process.env.COMPOSIO_API_KEY) { - composioAdapter = new ComposioAdapter({ - apiKey: process.env.COMPOSIO_API_KEY, - userId: process.env.COMPOSIO_USER_ID || "default", - }); - console.log(dim("[voice] Composio integration enabled")); - } - - // ── Telegram bot state ────────────────────────────────────────────── - let telegramToken = process.env.TELEGRAM_BOT_TOKEN || ""; - let telegramBotInfo: any = null; - let telegramPolling = false; - let telegramPollTimer: ReturnType | null = null; - let telegramOffset = 0; - // Allowed Telegram usernames — comma-separated in .env, empty = allow all - let telegramAllowedUsers = new Set( - (process.env.TELEGRAM_ALLOWED_USERS || "") - .split(",") - .map(s => s.trim().toLowerCase().replace(/^@/, "")) - .filter(Boolean), - ); - - let lastTelegramChatId: number | null = null; - - function stopTelegramPolling() { - telegramPolling = false; - if (telegramPollTimer) { clearTimeout(telegramPollTimer); telegramPollTimer = null; } - } - - /** Broadcast a message to all connected browser WebSocket clients */ - function broadcastToBrowsers(msg: ServerMessage) { - const payload = JSON.stringify(msg); - for (const client of wss.clients) { - if (client.readyState === 1) client.send(payload); - } - } - - // Wire log broadcast to WebSocket - logBroadcast = (entry) => broadcastToBrowsers({ type: "log_entry", entry } as ServerMessage); - - // ── Scheduler setup ──────────────────────────────────────────────── - const scheduleSendToBrowser = (msg: ServerMessage) => { - broadcastToBrowsers(msg); - appendMessage(opts.agentDir, activeBranch, msg); - }; - const headlessHandler = createToolHandler(scheduleSendToBrowser); - const schedulerOpts = { - agentDir: agentRoot, - model: opts.model, - env: opts.env, - runPrompt: headlessHandler, - broadcastToBrowsers, - appendToHistory: (msg: any) => appendMessage(opts.agentDir, activeBranch, msg), - }; - - async function downloadTelegramFile(fileId: string, agentDir: string): Promise<{ path: string; name: string } | null> { - try { - const fRes = await fetch(`https://api.telegram.org/bot${telegramToken}/getFile?file_id=${fileId}`); - const fData = await fRes.json() as any; - if (!fData.ok) return null; - const filePath = fData.result.file_path as string; - const ext = filePath.split(".").pop() || "jpg"; - const name = `telegram_${Date.now()}.${ext}`; - const dlUrl = `https://api.telegram.org/file/bot${telegramToken}/${filePath}`; - const dlRes = await fetch(dlUrl); - const buffer = Buffer.from(await dlRes.arrayBuffer()); - const wsDir = join(agentDir, "workspace"); - mkdirSync(wsDir, { recursive: true }); - const savePath = join(wsDir, name); - writeFileSync(savePath, buffer); - return { path: `workspace/${name}`, name }; - } catch { - return null; - } - } - - /** Collect all files recursively under a dir with their mtimes */ - function snapshotFiles(dir: string, base: string = ""): Map { - const result = new Map(); - try { - for (const name of readdirSync(dir)) { - if (name.startsWith(".") || name === "node_modules" || name === "dist") continue; - const full = join(dir, name); - const rel = base ? `${base}/${name}` : name; - try { - const st = statSync(full); - if (st.isDirectory()) { - for (const [k, v] of snapshotFiles(full, rel)) result.set(k, v); - } else if (st.isFile()) { - result.set(rel, st.mtimeMs); - } - } catch { /* skip */ } - } - } catch { /* skip */ } - return result; - } - - /** Find new or modified files by comparing snapshots */ - function diffSnapshots(before: Map, after: Map): string[] { - const changed: string[] = []; - for (const [path, mtime] of after) { - if (!before.has(path) || before.get(path)! < mtime) changed.push(path); - } - return changed; - } - - const SENDABLE_EXTS = new Set([ - "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "csv", "txt", "rtf", - "png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", - "zip", "tar", "gz", "json", "xml", "html", "css", "js", "ts", "py", "md", - "mp3", "mp4", "wav", "ogg", "webm", - ]); - - async function sendTelegramFile(chatId: number, filePath: string, agentDir: string, caption?: string) { - const abs = join(agentDir, filePath); - if (!existsSync(abs)) return; - const st = statSync(abs); - if (st.size > 50 * 1024 * 1024) return; // Telegram 50MB limit - const ext = filePath.split(".").pop()?.toLowerCase() || ""; - const isImage = /^(png|jpg|jpeg|gif|webp|bmp)$/.test(ext); - - const formBoundary = `----FormBoundary${Date.now()}`; - const fileData = readFileSync(abs); - const fileName = filePath.split("/").pop() || "file"; - - // Build multipart form - const fieldName = isImage ? "photo" : "document"; - const endpoint = isImage ? "sendPhoto" : "sendDocument"; - const parts: Buffer[] = []; - const nl = Buffer.from("\r\n"); - - // chat_id field - parts.push(Buffer.from(`--${formBoundary}\r\nContent-Disposition: form-data; name="chat_id"\r\n\r\n${chatId}`)); - parts.push(nl); - - // caption field - if (caption) { - const cap = caption.length > 1024 ? caption.slice(0, 1021) + "..." : caption; - parts.push(Buffer.from(`--${formBoundary}\r\nContent-Disposition: form-data; name="caption"\r\n\r\n${cap}`)); - parts.push(nl); - } - - // file field — strip charset from text MIMEs since Telegram expects bare types - const mime = fileTypeFor(fileName).mime.split(";")[0].trim(); - parts.push(Buffer.from(`--${formBoundary}\r\nContent-Disposition: form-data; name="${fieldName}"; filename="${fileName}"\r\nContent-Type: ${mime}\r\n\r\n`)); - parts.push(fileData); - parts.push(nl); - parts.push(Buffer.from(`--${formBoundary}--\r\n`)); - - const body = Buffer.concat(parts); - - try { - const resp = await fetch(`https://api.telegram.org/bot${telegramToken}/${endpoint}`, { - method: "POST", - headers: { "Content-Type": `multipart/form-data; boundary=${formBoundary}` }, - body, - }); - const rd = await resp.json() as any; - if (rd.ok) { - console.log(dim(`[telegram] Sent file: ${fileName}`)); - } else { - console.error(dim(`[telegram] Failed to send file ${fileName}: ${rd.description}`)); - } - } catch (err: any) { - console.error(dim(`[telegram] File send error: ${err.message}`)); - } - } - - function startTelegramPolling(agentDir: string, serverOpts: VoiceServerOptions) { - if (telegramPolling) return; - telegramPolling = true; - console.log(dim("[voice] Telegram polling started")); - - async function poll() { - if (!telegramPolling) return; - try { - const res = await fetch( - `https://api.telegram.org/bot${telegramToken}/getUpdates?offset=${telegramOffset}&timeout=30&allowed_updates=["message"]`, - ); - const data = await res.json() as any; - if (data.ok && data.result) { - for (const update of data.result) { - telegramOffset = update.update_id + 1; - const msg = update.message; - if (!msg) continue; - - const chatId = msg.chat.id; - lastTelegramChatId = chatId; - const fromName = msg.from?.first_name || "User"; - const fromUsername = (msg.from?.username || "").toLowerCase(); - - // Security: reject messages from unauthorized users - // Empty = block all, * = allow all, otherwise check username list - if (!telegramAllowedUsers.has("*")) { - if (telegramAllowedUsers.size === 0 || !telegramAllowedUsers.has(fromUsername)) { - console.log(dim(`[telegram] Blocked message from unauthorized user: @${fromUsername || "(no username)"} (${fromName})`)); - continue; - } - } - - let userText = msg.text || msg.caption || ""; - let imageContext = ""; - - // Handle photo messages - if (msg.photo && msg.photo.length > 0) { - const largest = msg.photo[msg.photo.length - 1]; - const dl = await downloadTelegramFile(largest.file_id, agentDir); - if (dl) { - imageContext = ` [Image saved to ${dl.path}]`; - // Notify browser of file change - broadcastToBrowsers({ type: "files_changed" } as any); - } - } - - // Handle document/file messages - if (msg.document) { - const dl = await downloadTelegramFile(msg.document.file_id, agentDir); - if (dl) { - imageContext = ` [File saved to ${dl.path}: ${msg.document.file_name || dl.name}]`; - broadcastToBrowsers({ type: "files_changed" } as any); - } - } - - if (!userText && !imageContext) continue; - - // ── Approval gate reply check ── - if (userText && handleApprovalReply(userText)) { - console.log(dim(`[telegram] Approval reply from ${fromName}: ${userText}`)); - const approvalMsg: ServerMessage = { type: "transcript", role: "user", text: `[Telegram] ${fromName}: ${userText}` }; - appendMessage(serverOpts.agentDir, activeBranch, approvalMsg); - broadcastToBrowsers(approvalMsg); - continue; - } - - const fullText = `${userText}${imageContext}`.trim(); - console.log(dim(`[telegram] ${fromName}: ${fullText.slice(0, 100)}`)); - - // ── Trigger check ── - if (userText) { - const trigger = matchTrigger(agentDir, "telegram", fromName, userText); - if (trigger) { - console.log(dim(`[triggers] Matched trigger ${trigger.id} for Telegram/${fromName}: "${userText.slice(0, 60)}" → "${trigger.reply.slice(0, 60)}"`)); - try { - await fetch(`https://api.telegram.org/bot${telegramToken}/sendMessage`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ chat_id: chatId, text: trigger.reply }), - }); - const triggerLog: ServerMessage = { type: "transcript", role: "assistant", text: `[Trigger → ${fromName}]: ${trigger.reply}` }; - appendMessage(serverOpts.agentDir, activeBranch, triggerLog); - broadcastToBrowsers(triggerLog); - } catch (err: any) { - console.error(dim(`[triggers] Telegram auto-reply failed: ${err.message}`)); - } - continue; // Skip agent processing for triggered messages - } - } - - // Save to shared chat history & broadcast to web UI - const userMsg: ServerMessage = { type: "transcript", role: "user", text: `[Telegram] ${fromName}: ${fullText}` }; - appendMessage(serverOpts.agentDir, activeBranch, userMsg); - broadcastToBrowsers(userMsg); - - // Send typing indicator - await fetch(`https://api.telegram.org/bot${telegramToken}/sendChatAction`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ chat_id: chatId, action: "typing" }), - }).catch(() => {}); - - // Snapshot files before agent runs - const beforeFiles = snapshotFiles(agentDir); - - // Run agent query - try { - const agentWorking: ServerMessage = { type: "agent_working", query: fullText }; - broadcastToBrowsers(agentWorking); - appendMessage(serverOpts.agentDir, activeBranch, agentWorking); - - const tgContext = await getAgentContext(agentDir, activeBranch); - const tgComposio = await getComposioContext(fullText); - let tgSystemPrompt = "You are an AI assistant responding to a Telegram user. " + - "Any files you create or modify will be AUTOMATICALLY sent back to the user on Telegram. " + - "When asked to create documents (PDF, Word, PPT, spreadsheets, images, markdown files, text files, etc.), " + - "write them to the workspace/ directory. The files will be delivered to the user immediately after you finish. " + - "Keep text responses concise since they appear in a chat interface."; - if (whatsappSock && whatsappConnected) { - tgSystemPrompt += " You can also send WhatsApp messages to contacts using the send_whatsapp_message tool. " + - "If you don't know a contact's number, ask the user or use list_whatsapp_contacts to check saved contacts."; - } - tgSystemPrompt += " You can set up auto-response triggers using create_trigger — e.g. 'when Kalps says hi on WhatsApp, reply hello friend'."; - tgSystemPrompt += "\n\n" + getCurrentDateTimeContext(); - if (tgComposio.promptSuffix) tgSystemPrompt += "\n\n" + tgComposio.promptSuffix; - if (tgContext) tgSystemPrompt += "\n\n" + tgContext; - const tgTools = [ - ...(whatsappSock && whatsappConnected ? createWhatsAppTools(whatsappSock, agentDir) : []), - ...createTriggerTools(agentDir), - ...tgComposio.tools, - ]; - const result = query({ - prompt: `[Telegram message from ${fromName}]: ${fullText}`, - dir: agentDir, - model: serverOpts.model, - env: serverOpts.env, - maxTurns: 10, - systemPrompt: tgSystemPrompt, - ...(tgTools.length ? { tools: tgTools } : {}), - }); - let reply = ""; - for await (const m of result) { - if (m.type === "assistant" && m.content) reply += m.content; - if (m.type === "tool_use") { - const toolMsg: ServerMessage = { type: "tool_call", toolName: m.toolName, args: m.args ?? {} }; - appendMessage(serverOpts.agentDir, activeBranch, toolMsg); - } - } - reply = reply.trim(); - - // Save agent response to shared history & broadcast - const doneMsg: ServerMessage = { type: "agent_done", result: reply.slice(0, 500) }; - appendMessage(serverOpts.agentDir, activeBranch, doneMsg); - broadcastToBrowsers(doneMsg); - - const assistantMsg: ServerMessage = { type: "transcript", role: "assistant", text: reply }; - appendMessage(serverOpts.agentDir, activeBranch, assistantMsg); - broadcastToBrowsers(assistantMsg); - - if (reply) { - // Split long messages (Telegram 4096 char limit) - const chunks: string[] = []; - for (let i = 0; i < reply.length; i += 4096) { - chunks.push(reply.slice(i, i + 4096)); - } - for (const chunk of chunks) { - await fetch(`https://api.telegram.org/bot${telegramToken}/sendMessage`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ chat_id: chatId, text: chunk, parse_mode: "Markdown" }), - }).catch(async () => { - // Fallback without Markdown if parsing fails - await fetch(`https://api.telegram.org/bot${telegramToken}/sendMessage`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ chat_id: chatId, text: chunk }), - }).catch(() => {}); - }); - } - } - - // Detect new/modified files and send them back to Telegram - const afterFiles = snapshotFiles(agentDir); - const newFiles = diffSnapshots(beforeFiles, afterFiles); - const filesToSend = newFiles.filter((f) => { - const ext = f.split(".").pop()?.toLowerCase() || ""; - // Skip chat history, internal files, and non-sendable types - if (f.startsWith(".gitagent/") || f.startsWith("node_modules/")) return false; - if (f === ".env" || f === ".gitignore") return false; - return SENDABLE_EXTS.has(ext); - }); - - for (const filePath of filesToSend) { - // Send upload_document action for each file - await fetch(`https://api.telegram.org/bot${telegramToken}/sendChatAction`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ chat_id: chatId, action: "upload_document" }), - }).catch(() => {}); - await sendTelegramFile(chatId, filePath, agentDir, filePath.split("/").pop()); - } - - // Notify browser of any file changes from agent - broadcastToBrowsers({ type: "files_changed" } as any); - } catch (err: any) { - console.error(dim(`[telegram] Agent error: ${err.message}`)); - await fetch(`https://api.telegram.org/bot${telegramToken}/sendMessage`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ chat_id: chatId, text: "Sorry, I encountered an error processing your message." }), - }).catch(() => {}); - } - } - } - } catch (err: any) { - console.error(dim(`[telegram] Poll error: ${err.message}`)); - } - if (telegramPolling) telegramPollTimer = setTimeout(poll, 500); - } - poll(); - } - - // Auto-connect if token is already configured - if (telegramToken) { - fetch(`https://api.telegram.org/bot${telegramToken}/getMe`) - .then((r) => r.json() as Promise) - .then((d) => { - if (d.ok) { - telegramBotInfo = d.result; - startTelegramPolling(agentRoot, opts); - console.log(dim(`[voice] Telegram bot connected: @${d.result.username}`)); - } - }) - .catch(() => {}); - } - - // ── WhatsApp state ───────────────────────────────────────────────── - let lastWhatsAppJid: string | null = null; - let whatsappSock: any = null; - let whatsappConnected = false; - let whatsappPhoneNumber: string | null = null; - let whatsappQrCode: string | null = null; - const whatsappSentIds = new Set(); - - // ── WhatsApp contacts store ──────────────────────────────────────── - interface WAContact { name: string; phone: string; jid: string } - - function contactsPath(agentDir: string): string { - return join(agentDir, ".gitagent", "whatsapp-contacts.json"); - } - - function loadContacts(agentDir: string): WAContact[] { - try { return JSON.parse(readFileSync(contactsPath(agentDir), "utf-8")); } - catch { return []; } - } - - function saveContacts(agentDir: string, contacts: WAContact[]): void { - const dir = join(agentDir, ".gitagent"); - mkdirSync(dir, { recursive: true }); - writeFileSync(contactsPath(agentDir), JSON.stringify(contacts, null, 2)); - } - - function findContact(agentDir: string, nameQuery: string): WAContact | undefined { - const q = nameQuery.toLowerCase(); - return loadContacts(agentDir).find(c => c.name.toLowerCase() === q || c.name.toLowerCase().includes(q)); - } - - function upsertContact(agentDir: string, contact: WAContact): void { - const contacts = loadContacts(agentDir); - const idx = contacts.findIndex(c => c.jid === contact.jid); - if (idx >= 0) contacts[idx] = contact; - else contacts.push(contact); - saveContacts(agentDir, contacts); - } - - /** Build WhatsApp tools that use the live Baileys socket */ - function createWhatsAppTools(sock: any, agentDir: string): GCToolDefinition[] { - return [ - { - name: "send_whatsapp_message", - description: "Send a WhatsApp message to a contact. You can specify either a phone number (with country code, e.g. '919876543210') or a contact name (if previously saved). The message will be sent immediately.", - inputSchema: { - type: "object", - properties: { - to: { type: "string", description: "Contact name or phone number (with country code, no '+' prefix, e.g. '919876543210')" }, - message: { type: "string", description: "Message text to send" }, - }, - required: ["to", "message"], - }, - handler: async (args: { to: string; message: string }) => { - let jid: string; - let displayName = args.to; - - // Try contact lookup first, then treat as phone number - const contact = findContact(agentDir, args.to); - if (contact) { - jid = contact.jid; - displayName = contact.name; - } else { - const digits = args.to.replace(/[^0-9]/g, ""); - if (!digits || digits.length < 7) { - return `Contact "${args.to}" not found. Use save_whatsapp_contact to save them first, or provide a phone number with country code (e.g. 919876543210).`; - } - jid = `${digits}@s.whatsapp.net`; - } - - const sent = await sock.sendMessage(jid, { text: args.message }); - if (sent?.key?.id) whatsappSentIds.add(sent.key.id); - console.log(dim(`[whatsapp] Sent message to ${displayName} (${jid}): ${args.message.slice(0, 80)}`)); - return `Message sent to ${displayName}.`; - }, - }, - { - name: "save_whatsapp_contact", - description: "Save a WhatsApp contact for future use. This lets you send messages by name instead of phone number.", - inputSchema: { - type: "object", - properties: { - name: { type: "string", description: "Contact name (e.g. 'Kalps')" }, - phone: { type: "string", description: "Phone number with country code, no '+' prefix (e.g. '919876543210')" }, - }, - required: ["name", "phone"], - }, - handler: async (args: { name: string; phone: string }) => { - const digits = args.phone.replace(/[^0-9]/g, ""); - const jid = `${digits}@s.whatsapp.net`; - upsertContact(agentDir, { name: args.name, phone: digits, jid }); - console.log(dim(`[whatsapp] Saved contact: ${args.name} → ${digits}`)); - return `Contact "${args.name}" saved with phone ${digits}.`; - }, - }, - { - name: "list_whatsapp_contacts", - description: "List all saved WhatsApp contacts.", - inputSchema: { type: "object", properties: {} }, - handler: async () => { - const contacts = loadContacts(agentDir); - if (!contacts.length) return "No saved contacts. Use save_whatsapp_contact to add one."; - return contacts.map(c => `${c.name}: ${c.phone}`).join("\n"); - }, - }, - ]; - } - - // ── Message triggers ────────────────────────────────────────────── - interface Trigger { - id: string; - from: string; // contact name or "*" for anyone - pattern: string; // substring/regex to match in message - reply: string; // auto-reply text (if set, sends directly without agent) - prompt?: string; // optional: run agent with this prompt instead of static reply - platform: string; // "whatsapp" | "telegram" | "*" - enabled: boolean; - } - - function triggersPath(agentDir: string): string { - return join(agentDir, ".gitagent", "triggers.json"); - } - - function loadTriggers(agentDir: string): Trigger[] { - try { return JSON.parse(readFileSync(triggersPath(agentDir), "utf-8")); } - catch { return []; } - } - - function saveTriggers(agentDir: string, triggers: Trigger[]): void { - const dir = join(agentDir, ".gitagent"); - mkdirSync(dir, { recursive: true }); - writeFileSync(triggersPath(agentDir), JSON.stringify(triggers, null, 2)); - } - - function matchTrigger(agentDir: string, platform: string, from: string, message: string): Trigger | undefined { - const triggers = loadTriggers(agentDir); - const fromLower = from.toLowerCase(); - const msgLower = message.toLowerCase(); - return triggers.find(t => { - if (!t.enabled) return false; - if (t.platform !== "*" && t.platform !== platform) return false; - if (t.from !== "*") { - // Match by contact name or phone number - const contact = findContact(agentDir, t.from); - if (contact) { - if (fromLower !== contact.jid && fromLower !== contact.phone && fromLower !== contact.name.toLowerCase()) return false; - } else if (fromLower !== t.from.toLowerCase()) return false; - } - // Pattern match — try regex first, fall back to substring - try { - if (new RegExp(t.pattern, "i").test(message)) return true; - } catch { - if (msgLower.includes(t.pattern.toLowerCase())) return true; - } - return false; - }); - } - - function createTriggerTools(agentDir: string): GCToolDefinition[] { - return [ - { - name: "create_trigger", - description: "Create an auto-response trigger. When a message matching the pattern arrives from the specified contact, the reply is sent automatically. Use from='*' to match anyone. Use platform='*' for all platforms.", - inputSchema: { - type: "object", - properties: { - from: { type: "string", description: "Contact name, phone number, or '*' for anyone" }, - pattern: { type: "string", description: "Text pattern to match (substring or regex)" }, - reply: { type: "string", description: "Auto-reply message to send" }, - platform: { type: "string", enum: ["whatsapp", "telegram", "*"], description: "Platform to trigger on (default: '*')" }, - }, - required: ["from", "pattern", "reply"], - }, - handler: async (args: { from: string; pattern: string; reply: string; platform?: string }) => { - const trigger: Trigger = { - id: Date.now().toString(36), - from: args.from, - pattern: args.pattern, - reply: args.reply, - platform: args.platform || "*", - enabled: true, - }; - const triggers = loadTriggers(agentDir); - triggers.push(trigger); - saveTriggers(agentDir, triggers); - console.log(dim(`[triggers] Created: when ${trigger.from} says "${trigger.pattern}" → "${trigger.reply}" (${trigger.platform})`)); - return `Trigger created (id: ${trigger.id}). When ${trigger.from} sends a message matching "${trigger.pattern}", I'll auto-reply: "${trigger.reply}"`; - }, - }, - { - name: "list_triggers", - description: "List all message triggers.", - inputSchema: { type: "object", properties: {} }, - handler: async () => { - const triggers = loadTriggers(agentDir); - if (!triggers.length) return "No triggers set up."; - return triggers.map(t => - `[${t.id}] ${t.enabled ? "ON" : "OFF"} | from: ${t.from} | pattern: "${t.pattern}" | reply: "${t.reply}" | platform: ${t.platform}` - ).join("\n"); - }, - }, - { - name: "delete_trigger", - description: "Delete a trigger by its ID.", - inputSchema: { - type: "object", - properties: { id: { type: "string", description: "Trigger ID to delete" } }, - required: ["id"], - }, - handler: async (args: { id: string }) => { - const triggers = loadTriggers(agentDir); - const idx = triggers.findIndex(t => t.id === args.id); - if (idx < 0) return `Trigger "${args.id}" not found.`; - const removed = triggers.splice(idx, 1)[0]; - saveTriggers(agentDir, triggers); - console.log(dim(`[triggers] Deleted: ${removed.id}`)); - return `Trigger "${removed.id}" deleted (was: ${removed.from} / "${removed.pattern}").`; - }, - }, - { - name: "toggle_trigger", - description: "Enable or disable a trigger by its ID.", - inputSchema: { - type: "object", - properties: { - id: { type: "string", description: "Trigger ID" }, - enabled: { type: "boolean", description: "true to enable, false to disable" }, - }, - required: ["id", "enabled"], - }, - handler: async (args: { id: string; enabled: boolean }) => { - const triggers = loadTriggers(agentDir); - const t = triggers.find(t => t.id === args.id); - if (!t) return `Trigger "${args.id}" not found.`; - t.enabled = args.enabled; - saveTriggers(agentDir, triggers); - return `Trigger "${t.id}" ${args.enabled ? "enabled" : "disabled"}.`; - }, - }, - ]; - } - - async function startWhatsApp(agentDir: string, serverOpts: VoiceServerOptions) { - const { - default: makeWASocket, - useMultiFileAuthState, - makeCacheableSignalKeyStore, - fetchLatestBaileysVersion, - DisconnectReason, - jidNormalizedUser, - } = await import("baileys"); - - const authDir = join(agentDir, ".gitagent/whatsapp-auth"); - mkdirSync(authDir, { recursive: true }); - - const { state, saveCreds } = await useMultiFileAuthState(authDir); - const { version } = await fetchLatestBaileysVersion(); - - const sock = makeWASocket({ - auth: { creds: state.creds, keys: makeCacheableSignalKeyStore(state.keys) }, - version, - browser: ["GitAgent", "cli", "0.3.1"], - printQRInTerminal: false, - syncFullHistory: false, - markOnlineOnConnect: false, - }); - whatsappSock = sock; - - sock.ev.on("connection.update", (update: any) => { - const { connection, lastDisconnect, qr } = update; - if (qr) { - whatsappQrCode = qr; - broadcastToBrowsers({ type: "whatsapp_qr", qr } as any); - console.log(dim("[whatsapp] QR code generated — scan with WhatsApp")); - } - if (connection === "open") { - whatsappConnected = true; - whatsappQrCode = null; - const jid = sock.user?.id || ""; - whatsappPhoneNumber = jid.replace(/:.*@/, "@").replace("@s.whatsapp.net", ""); - console.log(dim(`[whatsapp] Connected: ${whatsappPhoneNumber}`)); - broadcastToBrowsers({ type: "whatsapp_status", connected: true, phoneNumber: whatsappPhoneNumber } as any); - } - if (connection === "close") { - whatsappConnected = false; - whatsappQrCode = null; - const statusCode = (lastDisconnect?.error as any)?.output?.statusCode; - const loggedOut = statusCode === DisconnectReason.loggedOut; - console.log(dim(`[whatsapp] Disconnected (code=${statusCode}, loggedOut=${loggedOut})`)); - broadcastToBrowsers({ type: "whatsapp_status", connected: false } as any); - if (!loggedOut) { - // Auto-reconnect - setTimeout(() => startWhatsApp(agentDir, serverOpts).catch(() => {}), 3000); - } - } - }); - - sock.ev.on("creds.update", saveCreds); - - sock.ev.on("messages.upsert", async ({ messages, type }: any) => { - console.log(dim(`[whatsapp] upsert type=${type}, count=${messages.length}`)); - if (type !== "notify") return; - - const ownJid = sock.user?.id ? jidNormalizedUser(sock.user.id) : null; - // Also track our LID (Linked Identity) — WhatsApp may route self-DMs via LID - const ownLid = (sock as any).user?.lid?.replace(/:.*@/, "@") || null; - if (!ownJid) return; - - for (const msg of messages) { - console.log(dim(`[whatsapp] msg: remoteJid=${msg.key.remoteJid}, fromMe=${msg.key.fromMe}, ownJid=${ownJid}, ownLid=${ownLid}, id=${msg.key.id}`)); - // Skip agent's own replies - if (whatsappSentIds.has(msg.key.id!)) continue; - - const incomingText = msg.message?.conversation - || msg.message?.extendedTextMessage?.text || ""; - if (!incomingText) continue; - - const senderJid = msg.key.remoteJid!; - const isSelf = senderJid === ownJid || (ownLid && senderJid === ownLid); - - // ── Trigger check (runs on ALL incoming messages, not just self-DMs) ── - if (!isSelf && !msg.key.fromMe) { - // Resolve sender identity for trigger matching - const senderPhone = senderJid.replace("@s.whatsapp.net", ""); - const senderContact = loadContacts(agentDir).find(c => c.jid === senderJid || c.phone === senderPhone); - const senderName = senderContact?.name || senderPhone; - - const trigger = matchTrigger(agentDir, "whatsapp", senderContact?.name || senderJid, incomingText); - if (trigger) { - console.log(dim(`[triggers] Matched trigger ${trigger.id} for ${senderName}: "${incomingText.slice(0, 60)}" → "${trigger.reply.slice(0, 60)}"`)); - try { - const sent = await sock.sendMessage(senderJid, { text: trigger.reply }); - if (sent?.key?.id) whatsappSentIds.add(sent.key.id); - // Log to chat history - const triggerLog: ServerMessage = { type: "transcript", role: "assistant", text: `[Trigger → ${senderName}]: ${trigger.reply}` }; - appendMessage(serverOpts.agentDir, activeBranch, triggerLog); - broadcastToBrowsers(triggerLog); - } catch (err: any) { - console.error(dim(`[triggers] Failed to send auto-reply: ${err.message}`)); - } - } - continue; // Non-self messages are only processed for triggers - } - - // Only process self-DMs for agent interaction - if (!isSelf) continue; - - // ── Self-DM: full agent interaction ── - const text = incomingText; - const replyJid = senderJid; - lastWhatsAppJid = replyJid; - console.log(dim(`[whatsapp] Self-DM: ${text.slice(0, 100)}`)); - - // ── Approval gate reply check ── - if (handleApprovalReply(text)) { - console.log(dim(`[whatsapp] Approval reply: ${text}`)); - const approvalMsg: ServerMessage = { type: "transcript", role: "user", text: `[WhatsApp]: ${text}` }; - appendMessage(serverOpts.agentDir, activeBranch, approvalMsg); - broadcastToBrowsers(approvalMsg); - continue; - } - - // Broadcast to browser UI - const userMsg: ServerMessage = { type: "transcript", role: "user", text: `[WhatsApp]: ${text}` }; - appendMessage(serverOpts.agentDir, activeBranch, userMsg); - broadcastToBrowsers(userMsg); - - // Send typing presence - try { - await sock.presenceSubscribe(replyJid); - await sock.sendPresenceUpdate("composing", replyJid); - } catch { /* ignore */ } - - // Snapshot files before agent runs - const beforeFiles = snapshotFiles(agentDir); - - try { - const agentWorking: ServerMessage = { type: "agent_working", query: text }; - broadcastToBrowsers(agentWorking); - appendMessage(serverOpts.agentDir, activeBranch, agentWorking); - - const waContext = await getAgentContext(agentDir, activeBranch); - const waComposio = await getComposioContext(text); - let waSystemPrompt = "You are an AI assistant responding via WhatsApp. " + - "Any files you create or modify will be AUTOMATICALLY sent back to the user on WhatsApp. " + - "When asked to create documents or markdown files, write them to the workspace/ directory. " + - "Keep text responses concise since they appear in a chat interface. " + - "You can send WhatsApp messages to other people using the send_whatsapp_message tool. " + - "If you don't know a contact's number, ask the user or use list_whatsapp_contacts to check saved contacts. " + - "You can also set up auto-response triggers using create_trigger — e.g. 'when Kalps says hi, reply hello friend'."; - waSystemPrompt += "\n\n" + getCurrentDateTimeContext(); - if (waComposio.promptSuffix) waSystemPrompt += "\n\n" + waComposio.promptSuffix; - if (waContext) waSystemPrompt += "\n\n" + waContext; - const waTools = [...createWhatsAppTools(sock, agentDir), ...createTriggerTools(agentDir), ...waComposio.tools]; - const result = query({ - prompt: `[WhatsApp message]: ${text}`, - dir: agentDir, - model: serverOpts.model, - env: serverOpts.env, - maxTurns: 10, - systemPrompt: waSystemPrompt, - tools: waTools, - }); - let reply = ""; - for await (const m of result) { - if (m.type === "assistant" && m.content) reply += m.content; - } - reply = reply.trim(); - - // Save agent response to shared history & broadcast - const doneMsg: ServerMessage = { type: "agent_done", result: reply.slice(0, 500) }; - appendMessage(serverOpts.agentDir, activeBranch, doneMsg); - broadcastToBrowsers(doneMsg); - - const assistantMsg: ServerMessage = { type: "transcript", role: "assistant", text: reply }; - appendMessage(serverOpts.agentDir, activeBranch, assistantMsg); - broadcastToBrowsers(assistantMsg); - - // Send reply (chunk at 4000 chars for WhatsApp) - if (reply) { - const chunks: string[] = []; - for (let i = 0; i < reply.length; i += 4000) chunks.push(reply.slice(i, i + 4000)); - for (const chunk of chunks) { - const italicChunk = chunk.split("\n").map(line => line ? `_${line}_` : "").join("\n"); - const sent = await sock.sendMessage(replyJid, { text: `*GitAgent:*\n${italicChunk}` }); - if (sent?.key?.id) whatsappSentIds.add(sent.key.id); - } - } - - // Detect new/modified files and send them back - const afterFiles = snapshotFiles(agentDir); - const newFiles = diffSnapshots(beforeFiles, afterFiles).filter((f) => { - const ext = f.split(".").pop()?.toLowerCase() || ""; - if (f.startsWith(".gitagent/") || f.startsWith("node_modules/")) return false; - if (f === ".env" || f === ".gitignore") return false; - return SENDABLE_EXTS.has(ext); - }); - for (const filePath of newFiles) { - const abs = join(agentDir, filePath); - if (!existsSync(abs)) continue; - const buffer = readFileSync(abs); - const sent = await sock.sendMessage(replyJid, { - document: buffer, - fileName: filePath.split("/").pop() || "file", - mimetype: "application/octet-stream", - }); - if (sent?.key?.id) whatsappSentIds.add(sent.key.id); - } - - broadcastToBrowsers({ type: "files_changed" } as any); - } catch (err: any) { - console.error(dim(`[whatsapp] Agent error: ${err.message}`)); - try { - const sent = await sock.sendMessage(replyJid, { text: "*GitAgent:* _Sorry, I encountered an error processing your message._" }); - if (sent?.key?.id) whatsappSentIds.add(sent.key.id); - } catch { /* ignore */ } - } - } - }); - } - - function stopWhatsApp(clearAuth = false) { - if (whatsappSock) { - try { whatsappSock.end(undefined); } catch { /* ignore */ } - } - whatsappSock = null; - whatsappConnected = false; - whatsappPhoneNumber = null; - whatsappQrCode = null; - whatsappSentIds.clear(); - if (clearAuth) { - const authDir = join(agentRoot, ".gitagent/whatsapp-auth"); - try { rmSync(authDir, { recursive: true, force: true }); } catch { /* ignore */ } - } - } - - // Auto-connect WhatsApp if auth exists - const waAuthDir = join(agentRoot, ".gitagent/whatsapp-auth"); - if (existsSync(join(waAuthDir, "creds.json"))) { - startWhatsApp(agentRoot, opts).catch(() => {}); - } - - /** Resolve and validate a requested path stays within agentDir */ - function safePath(reqPath: string): string | null { - const abs = resolve(agentRoot, reqPath); - if (!abs.startsWith(agentRoot)) return null; - return abs; - } - - interface FileEntry { - name: string; - path: string; - type: "file" | "directory"; - mtime?: number; - children?: FileEntry[]; - } - - function listDir(dirPath: string, depth: number): FileEntry[] { - if (depth > 4) return []; - try { - const entries = readdirSync(dirPath); - const result: FileEntry[] = []; - for (const name of entries) { - if (name.startsWith(".") && HIDDEN_DIRS.has(name)) continue; - if (HIDDEN_DIRS.has(name)) continue; - const fullPath = join(dirPath, name); - const relPath = relative(agentRoot, fullPath); - try { - const st = statSync(fullPath); - if (st.isDirectory()) { - result.push({ - name, - path: relPath, - type: "directory", - children: listDir(fullPath, depth + 1), - }); - } else if (st.isFile()) { - result.push({ name, path: relPath, type: "file", mtime: st.mtimeMs }); - } - } catch { - // skip unreadable entries - } - } - // Sort: directories first, then alphabetical - result.sort((a, b) => { - if (a.type !== b.type) return a.type === "directory" ? -1 : 1; - return a.name.localeCompare(b.name); - }); - return result; - } catch { - return []; - } - } - - function readBody(req: IncomingMessage): Promise { - return new Promise((res, rej) => { - let body = ""; - req.on("data", (c: Buffer) => { body += c.toString(); }); - req.on("end", () => res(body)); - req.on("error", rej); - }); - } - - function jsonReply(res: ServerResponse, status: number, data: any) { - if (status >= 500 && data && data.error) { - console.error(`[http] 500 response: ${data.error}`); - } else if (status >= 400 && data && data.error) { - console.warn(`[http] ${status} response: ${data.error}`); - } - res.writeHead(status, { "Content-Type": "application/json" }); - res.end(JSON.stringify(data)); - } - - function escapeXml(s: string): string { - return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); - } - - // ── Password protection ────────────────────────────────────────── - // Auth gates the UI when GITAGENT_PASSWORD is set. GITAGENT_USERNAME is - // optional and defaults to "admin" when a password is configured. - const serverPassword = process.env.GITAGENT_PASSWORD || ""; - const serverUsername = process.env.GITAGENT_USERNAME || (serverPassword ? "admin" : ""); - const authCookieName = "gitagent_auth"; - - function generateAuthToken(): string { - // Hash username + password + salt so changing either invalidates existing cookies. - const { createHash } = require("crypto") as typeof import("crypto"); - return createHash("sha256") - .update(`${serverUsername}:${serverPassword}:_gitagent_session`) - .digest("hex") - .slice(0, 32); - } - - function isAuthenticated(req: IncomingMessage): boolean { - if (!serverPassword) return true; // No password set — open access - const cookie = req.headers.cookie || ""; - const match = cookie.match(new RegExp(`${authCookieName}=([^;]+)`)); - return match?.[1] === generateAuthToken(); - } - - function timingSafeEqualStr(a: string, b: string): boolean { - const { timingSafeEqual } = require("crypto") as typeof import("crypto"); - const ab = Buffer.from(a); - const bb = Buffer.from(b); - if (ab.length !== bb.length) return false; - return timingSafeEqual(ab, bb); - } - - const loginPageHtml = ` - -GitAgent — Login - - -`; - - // HTTP server - const httpServer: Server = createServer(async (req, res) => { - const reqStart = Date.now(); - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type"); - - if (req.method === "OPTIONS") { - res.writeHead(204); - return res.end(); - } - - const url = new URL(req.url || "/", `http://localhost:${port}`); - - // Log every HTTP request (skip UI + static paths to reduce noise; always log API/errors) - const isApi = url.pathname.startsWith("/api/"); - res.on("finish", () => { - if (isApi || res.statusCode >= 400) { - const dur = Date.now() - reqStart; - const level = res.statusCode >= 500 ? "error" : res.statusCode >= 400 ? "warn" : "log"; - const line = `[http] ${req.method} ${url.pathname} → ${res.statusCode} (${dur}ms)`; - if (level === "error") console.error(line); - else if (level === "warn") console.warn(line); - else console.log(line); - } - }); - req.on("error", (err) => console.error(`[http] Request error on ${req.method} ${url.pathname}: ${err.message}`)); - res.on("error", (err) => console.error(`[http] Response error on ${req.method} ${url.pathname}: ${err.message}`)); - - // ── Auth endpoints (always accessible) ── - if (url.pathname === "/api/auth" && req.method === "POST") { - let body: { username?: string; password?: string }; - try { - body = JSON.parse(await readBody(req)); - } catch { - return jsonReply(res, 400, { ok: false, error: "Invalid request" }); - } - const userOk = timingSafeEqualStr(String(body.username ?? ""), serverUsername); - const passOk = timingSafeEqualStr(String(body.password ?? ""), serverPassword); - if (userOk && passOk && serverPassword) { - res.setHeader("Set-Cookie", `${authCookieName}=${generateAuthToken()}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400`); - jsonReply(res, 200, { ok: true }); - } else { - jsonReply(res, 401, { ok: false, error: "Incorrect username or password" }); - } - return; - } - - // ── Password gate — block everything if not authenticated ── - if (!isAuthenticated(req)) { - if (url.pathname === "/health") { - // Health check always open for load balancers - jsonReply(res, 200, { status: "ok", auth: "required" }); - return; - } - res.writeHead(200, { "Content-Type": "text/html" }); - res.end(loginPageHtml); - return; - } - - if (url.pathname === "/health") { - jsonReply(res, 200, { status: "ok" }); - - } else if (url.pathname === "/api/vitals") { - jsonReply(res, 200, getVitalsSnapshot()); - - } else if (url.pathname === "/api/settings" && req.method === "GET") { - // Read current model from agent.yaml and key presence from .env - let model = ""; - try { - const yamlRaw = readFileSync(join(agentRoot, "agent.yaml"), "utf-8"); - const m = yamlRaw.match(/preferred:\s*["']?([^"'\n]+)["']?/); - if (m) model = m[1].trim(); - } catch { /* no agent.yaml */ } - const keys: Record = {}; - for (const k of ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY", "COMPOSIO_API_KEY"]) { - keys[k] = !!process.env[k]; - } - const baseUrl = process.env.GITAGENT_MODEL_BASE_URL || ""; - jsonReply(res, 200, { model, keys, baseUrl }); - - } else if (url.pathname === "/api/settings" && req.method === "PUT") { - try { - const body = JSON.parse(await readBody(req)); - - // Update .env with new keys - const envPath = join(agentRoot, ".env"); - let envContent = ""; - try { envContent = readFileSync(envPath, "utf-8"); } catch { /* new file */ } - - const envKeys = body.keys || {}; - for (const [key, val] of Object.entries(envKeys)) { - if (typeof val !== "string" || !val) continue; - process.env[key] = val; - const regex = new RegExp(`^${key}=.*$`, "m"); - if (regex.test(envContent)) { - envContent = envContent.replace(regex, `${key}=${val}`); - } else { - envContent += (envContent.endsWith("\n") || !envContent ? "" : "\n") + `${key}=${val}\n`; - } - } - writeFileSync(envPath, envContent, "utf-8"); - - // Update model in agent.yaml - if (body.model) { - const yamlPath = join(agentRoot, "agent.yaml"); - try { - let yamlContent = readFileSync(yamlPath, "utf-8"); - if (/preferred:\s*["']?[^"'\n]*["']?/.test(yamlContent)) { - yamlContent = yamlContent.replace( - /preferred:\s*["']?[^"'\n]*["']?/, - `preferred: "${body.model}"`, - ); - } - writeFileSync(yamlPath, yamlContent, "utf-8"); - } catch { /* no agent.yaml to update */ } - } - - // Update base URL in .env - if (body.baseUrl !== undefined) { - const baseUrlKey = "GITAGENT_MODEL_BASE_URL"; - if (body.baseUrl) { - process.env[baseUrlKey] = body.baseUrl; - const regex = new RegExp(`^${baseUrlKey}=.*$`, "m"); - if (regex.test(envContent)) { - envContent = envContent.replace(regex, `${baseUrlKey}=${body.baseUrl}`); - } else { - envContent += (envContent.endsWith("\n") || !envContent ? "" : "\n") + `${baseUrlKey}=${body.baseUrl}\n`; - } - } else { - delete process.env[baseUrlKey]; - envContent = envContent.replace(/^GITAGENT_MODEL_BASE_URL=.*\n?/m, ""); - } - writeFileSync(envPath, envContent, "utf-8"); - } - - console.log("[settings] Configuration updated — keys in process.env, model in agent.yaml"); - jsonReply(res, 200, { ok: true }); - } catch (err: any) { - jsonReply(res, 400, { error: err.message || "Invalid request" }); - } - - } else if (url.pathname === "/" || url.pathname === "/test") { - res.writeHead(200, { "Content-Type": "text/html" }); - res.end(buildUiHtml()); - - } else if (url.pathname === "/api/files" && req.method === "GET") { - // List files as a tree - const reqPath = url.searchParams.get("path") || "."; - const abs = safePath(reqPath); - if (!abs) return jsonReply(res, 403, { error: "Path outside workspace" }); - const tree = listDir(abs, 0); - jsonReply(res, 200, { root: relative(agentRoot, abs) || ".", entries: tree }); - - } else if (url.pathname === "/api/file" && req.method === "GET") { - // Read a file - const reqPath = url.searchParams.get("path"); - if (!reqPath) return jsonReply(res, 400, { error: "Missing path param" }); - const abs = safePath(reqPath); - if (!abs) return jsonReply(res, 403, { error: "Path outside workspace" }); - if (!existsSync(abs)) return jsonReply(res, 404, { error: "File not found" }); - try { - const st = statSync(abs); - if (st.size > 1024 * 1024) return jsonReply(res, 413, { error: "File too large (>1MB)" }); - const content = readFileSync(abs, "utf-8"); - jsonReply(res, 200, { path: reqPath, content }); - } catch (err: any) { - jsonReply(res, 500, { error: err.message }); - } - - } else if (url.pathname === "/api/file/raw" && req.method === "GET") { - // Serve raw file with correct MIME type, streaming + Range support. - // ?download=1 forces Content-Disposition: attachment. - const reqPath = url.searchParams.get("path"); - if (!reqPath) return jsonReply(res, 400, { error: "Missing path param" }); - const abs = safePath(reqPath); - if (!abs) return jsonReply(res, 403, { error: "Path outside workspace" }); - if (!existsSync(abs)) return jsonReply(res, 404, { error: "File not found" }); - try { - const info = fileTypeFor(reqPath); - const download = url.searchParams.get("download") === "1"; - streamFileWithRange(req, res, abs, { - mime: info.mime, - download, - filename: reqPath.split("/").pop() || undefined, - }); - } catch (err: any) { - jsonReply(res, 500, { error: err.message }); - } - - } else if (url.pathname === "/api/file/meta" && req.method === "GET") { - // File metadata (kind, mime, size, mtime) — UI calls this before deciding what to render. - const reqPath = url.searchParams.get("path"); - if (!reqPath) return jsonReply(res, 400, { error: "Missing path param" }); - const abs = safePath(reqPath); - if (!abs) return jsonReply(res, 403, { error: "Path outside workspace" }); - if (!existsSync(abs)) return jsonReply(res, 404, { error: "File not found" }); - try { - const st = statSync(abs); - const info = fileTypeFor(reqPath); - jsonReply(res, 200, { - path: reqPath, - name: reqPath.split("/").pop() || reqPath, - size: st.size, - mtime: st.mtimeMs, - kind: info.kind, - mime: info.mime, - }); - } catch (err: any) { - jsonReply(res, 500, { error: err.message }); - } - - } else if (url.pathname.startsWith("/preview/") && req.method === "GET") { - // Path-based file preview — relative URLs in HTML resolve against this prefix. - // e.g. /preview/workspace/site/index.html → → /preview/workspace/site/style.css - let relPath: string; - try { - relPath = decodeURIComponent(url.pathname.slice("/preview/".length)); - } catch { - return jsonReply(res, 400, { error: "Invalid path encoding" }); - } - if (!relPath) return jsonReply(res, 400, { error: "Missing path" }); - const abs = safePath(relPath); - if (!abs) return jsonReply(res, 403, { error: "Path outside workspace" }); - if (!existsSync(abs)) return jsonReply(res, 404, { error: "File not found" }); - try { - const info = fileTypeFor(relPath); - const extraHeaders: Record = {}; - if (info.kind === "html") { - // Sandbox is also applied on the iframe element; CSP is the real enforcement layer. - extraHeaders["Content-Security-Policy"] = - "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; frame-ancestors 'self'"; - extraHeaders["X-Frame-Options"] = "SAMEORIGIN"; - } - streamFileWithRange(req, res, abs, { - mime: info.mime, - filename: relPath.split("/").pop() || undefined, - extraHeaders, - }); - } catch (err: any) { - jsonReply(res, 500, { error: err.message }); - } - - } else if (url.pathname === "/api/file" && req.method === "PUT") { - // Write a file - const body = await readBody(req); - let parsed: { path: string; content: string }; - try { - parsed = JSON.parse(body); - } catch { - return jsonReply(res, 400, { error: "Invalid JSON body" }); - } - if (!parsed.path || parsed.content === undefined) return jsonReply(res, 400, { error: "Missing path or content" }); - const abs = safePath(parsed.path); - if (!abs) return jsonReply(res, 403, { error: "Path outside workspace" }); - try { - writeFileSync(abs, parsed.content, "utf-8"); - jsonReply(res, 200, { ok: true, path: parsed.path }); - } catch (err: any) { - jsonReply(res, 500, { error: err.message }); - } - - // ── Telegram bot routes ───────────────────────────────────────── - } else if (url.pathname === "/api/telegram/status" && req.method === "GET") { - jsonReply(res, 200, { - connected: telegramPolling, - botName: telegramBotInfo?.first_name || null, - botUsername: telegramBotInfo?.username || null, - hasToken: !!telegramToken, - allowedUsers: [...telegramAllowedUsers], - }); - - } else if (url.pathname === "/api/telegram/connect" && req.method === "POST") { - const body = await readBody(req); - try { - const parsed = JSON.parse(body); - if (parsed.token) telegramToken = parsed.token; - if (parsed.allowedUsers !== undefined) { - telegramAllowedUsers = new Set( - (parsed.allowedUsers as string).split(",") - .map((s: string) => s.trim().toLowerCase().replace(/^@/, "")) - .filter(Boolean), - ); - } - } catch { /* use existing token */ } - if (!telegramToken) return jsonReply(res, 400, { error: "No bot token provided" }); - - // Save token + allowed users to .env for persistence - const envPath = join(agentRoot, ".env"); - let envContent = ""; - try { envContent = readFileSync(envPath, "utf-8"); } catch { /* new file */ } - - // Save token - if (envContent.includes("TELEGRAM_BOT_TOKEN=")) { - envContent = envContent.replace(/^TELEGRAM_BOT_TOKEN=.*$/m, `TELEGRAM_BOT_TOKEN=${telegramToken}`); - } else { - envContent += `\nTELEGRAM_BOT_TOKEN=${telegramToken}\n`; - } - - // Save allowed users - const allowedStr = [...telegramAllowedUsers].join(","); - if (envContent.includes("TELEGRAM_ALLOWED_USERS=")) { - envContent = envContent.replace(/^TELEGRAM_ALLOWED_USERS=.*$/m, `TELEGRAM_ALLOWED_USERS=${allowedStr}`); - } else if (allowedStr) { - envContent += `TELEGRAM_ALLOWED_USERS=${allowedStr}\n`; - } - - writeFileSync(envPath, envContent, "utf-8"); - - // Validate token by calling getMe - try { - const meRes = await fetch(`https://api.telegram.org/bot${telegramToken}/getMe`); - const meData = await meRes.json() as any; - if (!meData.ok) return jsonReply(res, 400, { error: meData.description || "Invalid token" }); - telegramBotInfo = meData.result; - - // Start polling - startTelegramPolling(agentRoot, opts); - jsonReply(res, 200, { ok: true, botName: telegramBotInfo.first_name, botUsername: telegramBotInfo.username }); - } catch (err: any) { - jsonReply(res, 500, { error: err.message }); - } - - } else if (url.pathname === "/api/telegram/allowed-users" && req.method === "POST") { - const body = await readBody(req); - try { - const parsed = JSON.parse(body); - telegramAllowedUsers = new Set( - ((parsed.users as string) || "").split(",") - .map((s: string) => s.trim().toLowerCase().replace(/^@/, "")) - .filter(Boolean), - ); - // Persist to .env - const envPath = join(agentRoot, ".env"); - let envContent = ""; - try { envContent = readFileSync(envPath, "utf-8"); } catch { /* new file */ } - const allowedStr = [...telegramAllowedUsers].join(","); - if (envContent.includes("TELEGRAM_ALLOWED_USERS=")) { - envContent = envContent.replace(/^TELEGRAM_ALLOWED_USERS=.*$/m, `TELEGRAM_ALLOWED_USERS=${allowedStr}`); - } else if (allowedStr) { - envContent += `\nTELEGRAM_ALLOWED_USERS=${allowedStr}\n`; - } else { - envContent = envContent.replace(/^TELEGRAM_ALLOWED_USERS=.*\n?/m, ""); - } - writeFileSync(envPath, envContent, "utf-8"); - jsonReply(res, 200, { ok: true, allowedUsers: [...telegramAllowedUsers] }); - } catch (err: any) { - jsonReply(res, 400, { error: err.message }); - } - - } else if (url.pathname === "/api/telegram/disconnect" && req.method === "POST") { - stopTelegramPolling(); - telegramBotInfo = null; - jsonReply(res, 200, { ok: true }); - - // ── WhatsApp routes ───────────────────────────────────────────── - } else if (url.pathname === "/api/whatsapp/status" && req.method === "GET") { - jsonReply(res, 200, { - connected: whatsappConnected, - phoneNumber: whatsappPhoneNumber, - hasAuth: existsSync(join(agentRoot, ".gitagent/whatsapp-auth/creds.json")), - qrCode: whatsappQrCode, - }); - - } else if (url.pathname === "/api/whatsapp/connect" && req.method === "POST") { - if (whatsappConnected) return jsonReply(res, 200, { ok: true, connected: true, phoneNumber: whatsappPhoneNumber }); - try { - await startWhatsApp(agentRoot, opts); - jsonReply(res, 200, { ok: true, connecting: true }); - } catch (err: any) { - jsonReply(res, 500, { error: err.message }); - } - - } else if (url.pathname === "/api/whatsapp/disconnect" && req.method === "POST") { - let clearAuth = false; - try { - const body = await readBody(req); - const parsed = JSON.parse(body); - clearAuth = !!parsed.clearAuth; - } catch { /* no body is fine */ } - stopWhatsApp(clearAuth); - jsonReply(res, 200, { ok: true }); - - } else if (url.pathname === "/api/whatsapp/qr" && req.method === "GET") { - jsonReply(res, 200, { qrCode: whatsappQrCode, connected: whatsappConnected }); - - // ── Phone / Twilio webhook ────────────────────────────────────── - } else if (url.pathname === "/api/phone/webhook" && req.method === "POST") { - // Twilio sends SMS/voice webhooks here as application/x-www-form-urlencoded - const body = await readBody(req); - const params = new URLSearchParams(body); - const from = params.get("From") || ""; - const smsBody = params.get("Body") || ""; - const callStatus = params.get("CallStatus") || ""; - - if (smsBody) { - // Incoming SMS - console.log(dim(`[phone] SMS from ${from}: ${smsBody.slice(0, 100)}`)); - const userMsg: ServerMessage = { type: "transcript", role: "user", text: `[SMS ${from}]: ${smsBody}` }; - appendMessage(opts.agentDir, activeBranch, userMsg); - broadcastToBrowsers(userMsg); - - // Check triggers - const senderName = from.replace(/[^0-9]/g, ""); - const contact = loadContacts(opts.agentDir).find(c => c.phone === senderName || from.includes(c.phone)); - const trigger = matchTrigger(opts.agentDir, "phone", contact?.name || from, smsBody); - - if (trigger) { - console.log(dim(`[triggers] Phone trigger ${trigger.id}: "${smsBody.slice(0, 40)}" → "${trigger.reply.slice(0, 40)}"`)); - // Reply with TwiML - res.writeHead(200, { "Content-Type": "text/xml" }); - res.end(`${escapeXml(trigger.reply)}`); - const triggerLog: ServerMessage = { type: "transcript", role: "assistant", text: `[Trigger → ${from}]: ${trigger.reply}` }; - appendMessage(opts.agentDir, activeBranch, triggerLog); - broadcastToBrowsers(triggerLog); - return; - } - - // Run agent for non-triggered messages - try { - const phoneContext = await getAgentContext(opts.agentDir, activeBranch); - const phoneComposio = await getComposioContext(smsBody); - let phoneSystemPrompt = "You are an AI assistant responding to an SMS message via Twilio. " + - "Keep responses concise — SMS has character limits. Respond in plain text only."; - phoneSystemPrompt += "\n\n" + getCurrentDateTimeContext(); - if (phoneComposio.promptSuffix) phoneSystemPrompt += "\n\n" + phoneComposio.promptSuffix; - if (phoneContext) phoneSystemPrompt += "\n\n" + phoneContext; - const phoneTools = [ - ...createTriggerTools(opts.agentDir), - ...(whatsappSock && whatsappConnected ? createWhatsAppTools(whatsappSock, opts.agentDir) : []), - ...phoneComposio.tools, - ]; - const result = query({ - prompt: `[SMS from ${from}]: ${smsBody}`, - dir: opts.agentDir, - model: opts.model, - env: opts.env, - maxTurns: 5, - systemPrompt: phoneSystemPrompt, - ...(phoneTools.length ? { tools: phoneTools } : {}), - }); - let reply = ""; - for await (const m of result) { - if (m.type === "assistant" && m.content) reply += m.content; - } - reply = reply.trim().slice(0, 1600); // SMS limit - - const assistantMsg: ServerMessage = { type: "transcript", role: "assistant", text: `[SMS → ${from}]: ${reply}` }; - appendMessage(opts.agentDir, activeBranch, assistantMsg); - broadcastToBrowsers(assistantMsg); - - res.writeHead(200, { "Content-Type": "text/xml" }); - res.end(`${escapeXml(reply)}`); - } catch (err: any) { - console.error(dim(`[phone] Agent error: ${err.message}`)); - res.writeHead(200, { "Content-Type": "text/xml" }); - res.end(`Sorry, something went wrong.`); - } - } else if (callStatus) { - // Voice call webhook — just acknowledge for now - console.log(dim(`[phone] Call from ${from}, status: ${callStatus}`)); - res.writeHead(200, { "Content-Type": "text/xml" }); - res.end(`This number is managed by ${agentName}. Please send a text message instead.`); - } else { - res.writeHead(200, { "Content-Type": "text/xml" }); - res.end(``); - } - - // ── Composio OAuth callback ───────────────────────────────────── - } else if (url.pathname === "/api/composio/callback") { - // OAuth popup lands here after Composio processes the auth code. - // Send a message to the opener window and close the popup. - res.writeHead(200, { "Content-Type": "text/html" }); - res.end(`

Authentication complete. You can close this window.

`); - - // ── Chat branch API routes ────────────────────────────────────── - } else if (url.pathname === "/api/chat/list" && req.method === "GET") { - try { - const git = (cmd: string) => execSync(cmd, { cwd: agentRoot, encoding: "utf-8" }).trim(); - const current = git("git rev-parse --abbrev-ref HEAD"); - // List branches matching chat/* pattern, plus the current branch - let branches: string[]; - try { - branches = git("git branch --list 'chat/*' --sort=-committerdate --format='%(refname:short)|%(committerdate:relative)'") - .split("\n").filter(Boolean); - } catch { - branches = []; - } - const chats = branches.map((line) => { - const [branch, time] = line.split("|"); - const name = branch.replace("chat/", ""); - return { branch, name, time: time || "" }; - }); - // If current branch is not a chat/* branch, add it at the top - if (!current.startsWith("chat/")) { - chats.unshift({ branch: current, name: current, time: "current" }); - } - jsonReply(res, 200, { current, chats }); - } catch (err: any) { - jsonReply(res, 500, { error: err.message }); - } - - } else if (url.pathname === "/api/chat/new" && req.method === "POST") { - try { - const git = (cmd: string) => execSync(cmd, { cwd: agentRoot, encoding: "utf-8" }).trim(); - // Generate branch name: chat/YYYY-MM-DD-HHMMSS - const now = new Date(); - const pad = (n: number) => String(n).padStart(2, "0"); - const branch = `chat/${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`; - // Stage and commit any pending changes on current branch - try { - git("git add -A"); - git('git commit -m "auto-save before new chat" --allow-empty'); - } catch { - // No changes to commit, that's fine - } - // Create and switch to new branch - git(`git checkout -b ${branch}`); - activeBranch = branch; - jsonReply(res, 200, { branch }); - } catch (err: any) { - jsonReply(res, 500, { error: err.message }); - } - - } else if (url.pathname === "/api/chat/switch" && req.method === "POST") { - try { - const body = await readBody(req); - const { branch } = JSON.parse(body); - if (!branch) return jsonReply(res, 400, { error: "Missing branch" }); - const git = (cmd: string) => execSync(cmd, { cwd: agentRoot, encoding: "utf-8" }).trim(); - // Auto-save current branch - try { - git("git add -A"); - git('git commit -m "auto-save before switching chat" --allow-empty'); - } catch {} - git(`git checkout ${branch}`); - activeBranch = branch; - jsonReply(res, 200, { branch }); - } catch (err: any) { - jsonReply(res, 500, { error: err.message }); - } - - } else if (url.pathname === "/api/chat/delete" && req.method === "POST") { - try { - const body = await readBody(req); - const { branch } = JSON.parse(body); - if (!branch) return jsonReply(res, 400, { error: "Missing branch" }); - const git = (cmd: string) => execSync(cmd, { cwd: agentRoot, encoding: "utf-8" }).trim(); - const current = git("git rev-parse --abbrev-ref HEAD"); - if (branch === current) return jsonReply(res, 400, { error: "Cannot delete the active branch" }); - git(`git branch -D ${branch}`); - deleteHistory(opts.agentDir, branch); - jsonReply(res, 200, { ok: true }); - } catch (err: any) { - jsonReply(res, 500, { error: err.message }); - } - - } else if (url.pathname === "/api/chat/history" && req.method === "GET") { - const branch = url.searchParams.get("branch"); - if (!branch) return jsonReply(res, 400, { error: "Missing branch param" }); - const messages = loadHistory(opts.agentDir, branch); - jsonReply(res, 200, { branch, messages }); - - // ── Composio API routes ───────────────────────────────────────── - } else if (url.pathname === "/api/composio/toolkits" && req.method === "GET") { - if (!composioAdapter) return jsonReply(res, 501, { error: "Composio not configured" }); - try { - const toolkits = await composioAdapter.getToolkits(); - jsonReply(res, 200, toolkits); - } catch (err: any) { - jsonReply(res, 502, { error: err.message }); - } - - } else if (url.pathname === "/api/composio/connect" && req.method === "POST") { - if (!composioAdapter) return jsonReply(res, 501, { error: "Composio not configured" }); - const body = await readBody(req); - let parsed: { toolkit: string; redirectUrl?: string }; - try { parsed = JSON.parse(body); } catch { return jsonReply(res, 400, { error: "Invalid JSON" }); } - if (!parsed.toolkit) return jsonReply(res, 400, { error: "Missing toolkit" }); - try { - const result = await composioAdapter.connect(parsed.toolkit, parsed.redirectUrl); - jsonReply(res, 200, result); - } catch (err: any) { - jsonReply(res, 502, { error: err.message }); - } - - } else if (url.pathname === "/api/composio/connections" && req.method === "GET") { - if (!composioAdapter) return jsonReply(res, 501, { error: "Composio not configured" }); - try { - const connections = await composioAdapter.getConnections(); - jsonReply(res, 200, connections); - } catch (err: any) { - jsonReply(res, 502, { error: err.message }); - } - - } else if (url.pathname.match(/^\/api\/composio\/connections\/[^/]+$/) && req.method === "DELETE") { - if (!composioAdapter) return jsonReply(res, 501, { error: "Composio not configured" }); - const connId = url.pathname.split("/").pop()!; - try { - await composioAdapter.disconnect(connId); - jsonReply(res, 200, { ok: true }); - } catch (err: any) { - jsonReply(res, 502, { error: err.message }); - } - - // ── SkillFlows API ──────────────────────────────────────────── - } else if (url.pathname === "/api/skills/list" && req.method === "GET") { - try { - const skills = await discoverSkills(agentRoot); - jsonReply(res, 200, { skills }); - } catch (err: any) { - jsonReply(res, 500, { error: err.message }); - } - - } else if (url.pathname === "/api/flows/list" && req.method === "GET") { - try { - const workflows = await discoverWorkflows(agentRoot); - const flows = workflows.filter((w) => w.type === "flow"); - jsonReply(res, 200, { flows }); - } catch (err: any) { - jsonReply(res, 500, { error: err.message }); - } - - } else if (url.pathname === "/api/flows/save" && req.method === "POST") { - const body = await readBody(req); - let parsed: { name: string; description: string; steps: { skill: string; prompt: string; channel?: string }[] }; - try { parsed = JSON.parse(body); } catch { return jsonReply(res, 400, { error: "Invalid JSON" }); } - if (!parsed.name || !parsed.steps?.length) return jsonReply(res, 400, { error: "Missing name or steps" }); - try { - await saveFlowDefinition(agentRoot, parsed); - jsonReply(res, 200, { ok: true }); - } catch (err: any) { - jsonReply(res, 400, { error: err.message }); - } - - } else if (url.pathname === "/api/flows/delete" && req.method === "DELETE") { - const body = await readBody(req); - let parsed: { name: string }; - try { parsed = JSON.parse(body); } catch { return jsonReply(res, 400, { error: "Invalid JSON" }); } - if (!parsed.name) return jsonReply(res, 400, { error: "Missing name" }); - try { - await deleteFlowDefinition(agentRoot, parsed.name); - jsonReply(res, 200, { ok: true }); - } catch (err: any) { - jsonReply(res, 500, { error: err.message }); - } - - // ── Scheduler API ────────────────────────────────────────────── - } else if (url.pathname === "/api/schedules/list" && req.method === "GET") { - try { - const schedules = await discoverSchedules(agentRoot); - jsonReply(res, 200, { schedules }); - } catch (err: any) { - jsonReply(res, 500, { error: err.message }); - } - - } else if (url.pathname === "/api/schedules/save" && req.method === "POST") { - const body = await readBody(req); - let parsed: { id: string; prompt: string; cron?: string; mode?: string; runAt?: string; enabled?: boolean }; - try { parsed = JSON.parse(body); } catch { return jsonReply(res, 400, { error: "Invalid JSON" }); } - if (!parsed.id || !parsed.prompt) return jsonReply(res, 400, { error: "Missing id or prompt" }); - const mode = parsed.mode === "once" ? "once" as const : "repeat" as const; - if (mode === "once" && parsed.runAt) { - // runAt mode — validate the datetime is in the future - const runAtDate = new Date(parsed.runAt); - if (isNaN(runAtDate.getTime())) return jsonReply(res, 400, { error: "Invalid runAt datetime" }); - } else { - // cron mode — validate expression - if (!parsed.cron) return jsonReply(res, 400, { error: "Missing cron expression" }); - if (!cron.validate(parsed.cron)) return jsonReply(res, 400, { error: "Invalid cron expression" }); - } - try { - await saveSchedule(agentRoot, { - id: parsed.id, - prompt: parsed.prompt, - cron: parsed.cron || "", - mode, - ...(parsed.runAt ? { runAt: parsed.runAt } : {}), - enabled: parsed.enabled !== false, - createdAt: new Date().toISOString(), - }); - await reloadSchedules(schedulerOpts); - jsonReply(res, 200, { ok: true }); - } catch (err: any) { - jsonReply(res, 400, { error: err.message }); - } - - } else if (url.pathname === "/api/schedules/delete" && req.method === "DELETE") { - const body = await readBody(req); - let parsed: { id: string }; - try { parsed = JSON.parse(body); } catch { return jsonReply(res, 400, { error: "Invalid JSON" }); } - if (!parsed.id) return jsonReply(res, 400, { error: "Missing id" }); - try { - await deleteSchedule(agentRoot, parsed.id); - await reloadSchedules(schedulerOpts); - jsonReply(res, 200, { ok: true }); - } catch (err: any) { - jsonReply(res, 500, { error: err.message }); - } - - } else if (url.pathname === "/api/schedules/toggle" && req.method === "POST") { - const body = await readBody(req); - let parsed: { id: string; enabled: boolean }; - try { parsed = JSON.parse(body); } catch { return jsonReply(res, 400, { error: "Invalid JSON" }); } - if (!parsed.id) return jsonReply(res, 400, { error: "Missing id" }); - try { - await updateScheduleMeta(agentRoot, parsed.id, { enabled: parsed.enabled }); - await reloadSchedules(schedulerOpts); - jsonReply(res, 200, { ok: true }); - } catch (err: any) { - jsonReply(res, 500, { error: err.message }); - } - - } else if (url.pathname === "/api/schedules/run" && req.method === "POST") { - const body = await readBody(req); - let parsed: { id: string }; - try { parsed = JSON.parse(body); } catch { return jsonReply(res, 400, { error: "Invalid JSON" }); } - if (!parsed.id) return jsonReply(res, 400, { error: "Missing id" }); - try { - const schedules = await discoverSchedules(agentRoot); - const schedule = schedules.find((s) => s.id === parsed.id); - if (!schedule) return jsonReply(res, 404, { error: "Schedule not found" }); - jsonReply(res, 200, { ok: true, message: "Job triggered" }); - executeScheduledJob(schedule, schedulerOpts); - } catch (err: any) { - jsonReply(res, 500, { error: err.message }); - } - - } else if (url.pathname === "/api/schedules/logs" && req.method === "GET") { - const id = url.searchParams.get("id"); - if (!id) return jsonReply(res, 400, { error: "Missing id param" }); - try { - const logFile = join(agentRoot, ".gitagent", "schedule-logs", `${id}.jsonl`); - const raw = readFileSync(logFile, "utf-8"); - const lines = raw.trim().split("\n").filter(Boolean); - const entries = lines.slice(-50).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); - jsonReply(res, 200, { entries }); - } catch { - jsonReply(res, 200, { entries: [] }); - } - - // ── Logs API ──────────────────────────────────────────────────── - } else if (url.pathname === "/api/logs" && req.method === "GET") { - const sinceParam = url.searchParams.get("since"); - const sourceFilter = url.searchParams.get("source") || ""; - const levelFilter = url.searchParams.get("level") || ""; - const searchFilter = (url.searchParams.get("q") || "").toLowerCase(); - let entries = sinceParam ? logBuffer.since(parseInt(sinceParam, 10)) : logBuffer.all(); - if (sourceFilter) entries = entries.filter(e => e.source === sourceFilter); - if (levelFilter) entries = entries.filter(e => e.level === levelFilter); - if (searchFilter) entries = entries.filter(e => e.message.toLowerCase().includes(searchFilter)); - jsonReply(res, 200, { entries }); - - // ── Skills Marketplace proxy ──────────────────────────────────── - } else if (url.pathname === "/api/skills-mp/proxy" && req.method === "GET") { - const proxyPath = url.searchParams.get("path") || "/"; - // Forward all query params except "path" to skills.sh - const forwardParams = new URLSearchParams(url.searchParams); - forwardParams.delete("path"); - const qs = forwardParams.toString(); - const targetUrl = `https://skills.sh${proxyPath.startsWith("/") ? proxyPath : "/" + proxyPath}${qs ? (proxyPath.includes("?") ? "&" : "?") + qs : ""}`; - - try { - const proxyRes = await fetch(targetUrl, { - headers: { - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - }, - redirect: "follow", - }); - - const contentType = proxyRes.headers.get("content-type") || ""; - - // Non-HTML resources: pass through directly - if (!contentType.includes("text/html")) { - const buffer = Buffer.from(await proxyRes.arrayBuffer()); - res.writeHead(proxyRes.status, { - "Content-Type": contentType, - "Cache-Control": proxyRes.headers.get("cache-control") || "public, max-age=3600", - "Access-Control-Allow-Origin": "*", - }); - res.end(buffer); - return; - } - - let html = await proxyRes.text(); - - // Rewrite relative src/href to absolute skills.sh URLs so assets load correctly - // (Do NOT rewrite href for navigation links — that breaks React hydration. - // Navigation is handled by client-side click/history interception instead.) - html = html.replace(/src="\/(?!\/)/g, 'src="https://skills.sh/'); - html = html.replace(/src='\/(?!\/)/g, "src='https://skills.sh/"); - // Rewrite stylesheet/preload hrefs to load from skills.sh - html = html.replace(/href="\/_(next|static)\//g, 'href="https://skills.sh/_$1/'); - html = html.replace(/href='\/_(next|static)\//g, "href='https://skills.sh/_$1/"); - - // Inject our custom script before - const injectedScript = ` -`; - - html = html.replace(/<\/body>/i, injectedScript + ""); - - res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Access-Control-Allow-Origin": "*" }); - res.end(html); - } catch (err: any) { - // Fallback if skills.sh is unreachable - res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); - res.end(` - -

Skills Marketplace Unavailable

Could not reach skills.sh: ${err.message}

-

Open skills.sh in a new tab

`); - } - - // ── Skills Marketplace installed list ──────────────────────────── - } else if (url.pathname === "/api/skills-mp/installed" && req.method === "GET") { - try { - const lockPath = join(agentRoot, "skills-lock.json"); - if (existsSync(lockPath)) { - const lock = JSON.parse(readFileSync(lockPath, "utf-8")); - const skills = lock.skills || {}; - const names = Object.keys(skills); - // Build a set of sources (repo slugs) that have installed skills - const sources = [...new Set(Object.values(skills).map((s: any) => s.source))]; - jsonReply(res, 200, { installed: names, sources }); - } else { - jsonReply(res, 200, { installed: [], sources: [] }); - } - } catch (err: any) { - jsonReply(res, 200, { installed: [], sources: [] }); - } - - // ── Skills Marketplace install ────────────────────────────────── - } else if (url.pathname === "/api/skills-mp/install" && req.method === "POST") { - let body = ""; - for await (const chunk of req) body += chunk; - try { - const { source } = JSON.parse(body) as { source: string }; - if (!source) return jsonReply(res, 400, { error: "Missing source" }); - - // Shell out to the skills CLI — it handles all install logic - const cleanSource = source.replace(/^https?:\/\/github\.com\//, ""); - const skillsDir = join(agentRoot, "skills"); - const before = new Set(existsSync(skillsDir) ? readdirSync(skillsDir) : []); - - execSync(`npx -y skills add -y ${cleanSource} --agent openclaw`, { - cwd: agentRoot, - encoding: "utf-8", - timeout: 120000, - }); - - // Detect which skill directories were added (symlinked into skills/) - const after = existsSync(skillsDir) ? readdirSync(skillsDir) : []; - const added = after.filter(d => !before.has(d)); - const skillNames = added.length ? added : [cleanSource.split("/")[1] || cleanSource]; - console.log(dim(`[voice] Installed skill(s): ${skillNames.join(", ")} via npx skills add`)); - broadcastToBrowsers({ type: "files_changed" } as any); - jsonReply(res, 200, { ok: true, skillName: skillNames.join(", "), path: `skills/`, installed: skillNames }); - } catch (err: any) { - jsonReply(res, 500, { error: err.message }); - } - - } else { - res.writeHead(404); - res.end(); - } - }); - - httpServer.on("error", (err: Error) => { - console.error(`[http] Server error: ${err.message}\n${err.stack}`); - }); - httpServer.on("clientError", (err: any, socket: any) => { - console.error(`[http] Client error: ${err.message}`); - try { socket.destroy(); } catch { /* no-op */ } - }); - - // WebSocket server — adapter-agnostic proxy - const wss = new WebSocketServer({ server: httpServer }); - wss.on("error", (err: Error) => { - console.error(`[voice] WebSocket server error: ${err.message}\n${err.stack}`); - }); - - wss.on("connection", async (browserWs: WS, req: IncomingMessage) => { - // Check auth on WebSocket connections - if (!isAuthenticated(req)) { - console.warn(`[voice] Browser WS rejected (unauthorized) from ${req.socket.remoteAddress}`); - browserWs.close(4401, "Unauthorized"); - return; - } - const remote = req.socket.remoteAddress || "unknown"; - console.log(dim(`[voice] Browser connected from ${remote}`)); - browserWs.on("error", (err: Error) => { - console.error(`[voice] Browser WS error (${remote}): ${err.message}`); - }); - - // ── Per-connection frame buffer + moment capture state ────────── - let latestVideoFrame: { frame: string; mimeType: string; ts: number } | null = null; - let lastFrameWriteTs = 0; - let latestScreenFrame: { frame: string; mimeType: string; ts: number } | null = null; - let lastScreenWriteTs = 0; - let lastMomentCaptureTs = 0; - const FRAME_WRITE_INTERVAL = 2000; // Write temp frame to disk every 2s - const MOMENT_COOLDOWN = 60000; // 60s between auto-captures - const moodCounts: MoodCounts = { happy: 0, frustrated: 0, curious: 0, excited: 0, calm: 0 }; - let sessionMessageCount = 0; - - // Inject shared context (memory + conversation summary) into voice LLM instructions - const voiceContext = await getVoiceContext(opts.agentDir, activeBranch); - let instructions = opts.adapterConfig.instructions || ""; - if (voiceContext) { - instructions += "\n\n" + voiceContext; - } - if (CLOUD_MODE) { - instructions += CLOUD_VOICE_SUFFIX; - } - - // Inject Composio awareness into adapter instructions so the voice LLM - // never tells the user "I can't access" external services - const adapterOpts = composioAdapter ? { - ...opts, - adapterConfig: { - ...opts.adapterConfig, - instructions: instructions + - " The agent has FULL access to external services via Composio — Gmail, Google Calendar, GitHub, Slack, and more. " + - "When the user asks to send emails, check calendars, or interact with any external service, ALWAYS use run_agent to handle it. " + - "NEVER say you can't access these services or that you don't have these tools. The agent has them. Just call run_agent.", - }, - } : { - ...opts, - adapterConfig: { - ...opts.adapterConfig, - instructions, - }, - }; - let adapter: MultimodalAdapter | null = opts.adapterConfig.apiKey ? createAdapter(adapterOpts) : null; - const sendToBrowser = (msg: ServerMessage) => { - safeSend(browserWs, JSON.stringify(msg)); - appendMessage(opts.agentDir, activeBranch, msg); - // Track mood from user transcripts - if (msg.type === "transcript" && msg.role === "user" && !msg.partial) { - sessionMessageCount++; - const mood = detectMood(msg.text); - if (mood) moodCounts[mood]++; - } - // Detect personal info in voice transcripts and save to memory - if (msg.type === "transcript" && msg.role === "user" && !msg.partial && isMemoryWorthy(msg.text)) { - saveMemoryInBackground(msg.text, opts.agentDir, opts.model, opts.env, () => { - broadcastToBrowsers({ type: "memory_saving", status: "start", text: msg.text.slice(0, 60) }); - }, () => { - broadcastToBrowsers({ type: "memory_saving", status: "done" }); - safeSend(browserWs, JSON.stringify({ type: "files_changed" })); - }); - } - // Auto-capture photo on memorable moments (with 60s cooldown) - if (msg.type === "transcript" && msg.role === "user" && !msg.partial && isMomentWorthy(msg.text)) { - const now = Date.now(); - if (now - lastMomentCaptureTs >= MOMENT_COOLDOWN) { - lastMomentCaptureTs = now; - // Use buffered frame if available and fresh (<5s) - let frameBuffer: Buffer | undefined; - if (latestVideoFrame && (now - latestVideoFrame.ts) < 5000) { - frameBuffer = Buffer.from(latestVideoFrame.frame, "base64"); - } - capturePhoto(agentRoot, msg.text.slice(0, 60), frameBuffer).catch((err) => { - console.error(dim(`[voice] Auto photo capture failed: ${err.message}`)); - }); - } - } - }; - - if (adapter) { - try { - await adapter.connect({ - toolHandler: createToolHandler(sendToBrowser), - onMessage: sendToBrowser, - }); - console.log(dim(`[voice] Adapter ready (${opts.adapter})`)); - } catch (err: any) { - console.error(dim(`[voice] Adapter connection failed: ${err.message}`)); - safeSend(browserWs, JSON.stringify({ type: "error", message: `Voice connection failed: ${err.message}` })); - adapter = null; // Fall back to text-only - } - } - if (!adapter) { - safeSend(browserWs, JSON.stringify({ - type: "transcript", role: "assistant", - text: "Voice mode unavailable — no API key set. You can still chat via text.", - })); - } - - // Parse browser messages into ClientMessage and forward to adapter - browserWs.on("message", async (data) => { - try { - const msg = JSON.parse(data.toString()) as ClientMessage; - - // Buffer video frames and throttle-write to disk for capture_photo tool - if (msg.type === "video_frame") { - const source = msg.source || "camera"; - if (source === "screen") { - latestScreenFrame = { frame: msg.frame, mimeType: msg.mimeType, ts: Date.now() }; - const now = Date.now(); - if (now - lastScreenWriteTs >= 3000) { - lastScreenWriteTs = now; - const frameBuffer = Buffer.from(msg.frame, "base64"); - const framePath = join(agentRoot, LATEST_SCREEN_FILE); - writeFile(framePath, frameBuffer).catch(() => {}); - } - } else { - latestVideoFrame = { frame: msg.frame, mimeType: msg.mimeType, ts: Date.now() }; - const now = Date.now(); - if (now - lastFrameWriteTs >= FRAME_WRITE_INTERVAL) { - lastFrameWriteTs = now; - const frameBuffer = Buffer.from(msg.frame, "base64"); - const framePath = join(agentRoot, LATEST_FRAME_FILE); - writeFile(framePath, frameBuffer).catch(() => {}); - } - } - } - - if (msg.type === "text") { - appendMessage(opts.agentDir, activeBranch, { type: "transcript", role: "user", text: msg.text }); - - // Detect @flow-name triggers - const flowMatch = msg.text.match(/@([a-z0-9]+(?:-[a-z0-9]+)*)/); - if (flowMatch) { - try { - const workflows = await discoverWorkflows(agentRoot); - const flow = workflows.find((f) => f.name === flowMatch[1] && f.type === "flow"); - if (flow) { - const userContext = msg.text.replace(/@[a-z0-9-]+/, "").trim(); - executeFlow(flow.name, userContext, sendToBrowser).catch((err) => { - sendToBrowser({ type: "transcript", role: "assistant", text: `Flow error: ${err.message}` }); - }); - return; // skip adapter.send() - } - } catch { - // Fall through to normal send if flow detection fails - } - } - - // Detect personal info and save to memory in background - if (isMemoryWorthy(msg.text)) { - saveMemoryInBackground(msg.text, opts.agentDir, opts.model, opts.env, () => { - broadcastToBrowsers({ type: "memory_saving", status: "start", text: msg.text.slice(0, 60) }); - }, () => { - broadcastToBrowsers({ type: "memory_saving", status: "done" }); - safeSend(browserWs, JSON.stringify({ type: "files_changed" })); - }); - } - - // Text-only mode — call agent directly when no voice adapter - if (!adapter) { - const handler = createToolHandler(sendToBrowser); - handler(msg.text).then((result) => { - safeSend(browserWs, JSON.stringify({ type: "agent_done", result })); - appendMessage(opts.agentDir, activeBranch, { type: "transcript", role: "assistant", text: result }); - safeSend(browserWs, JSON.stringify({ type: "files_changed" })); - }).catch((err: any) => { - safeSend(browserWs, JSON.stringify({ type: "error", message: err.message })); - }); - return; - } - } else if (msg.type === "file") { - // Save uploaded file to disk so the text agent can use it - const uploadsDir = join(agentRoot, "workspace"); - mkdirSync(uploadsDir, { recursive: true }); - const safeName = (msg as any).name.replace(/[^a-zA-Z0-9._-]/g, "_"); - const filePath = join(uploadsDir, safeName); - writeFileSync(filePath, Buffer.from((msg as any).data, "base64")); - const relPath = relative(agentRoot, filePath); - console.log(dim(`[voice] Saved uploaded file: ${relPath}`)); - - // Inject path into message so voice LLM tells the agent where the file is - const userText = (msg as any).text || ""; - (msg as any).text = `${userText}${userText ? " " : ""}[File saved to: ${relPath} (absolute: ${filePath})]`; - - appendMessage(opts.agentDir, activeBranch, { - type: "transcript", role: "user", - text: `${userText} [Attached: ${safeName} → ${relPath}]`.trim(), - }); - } - adapter?.send(msg); - } catch (err: any) { - console.error(`[voice] WS message handler error: ${err?.message || err}${err?.stack ? "\n" + err.stack : ""}`); - } - }); - - browserWs.on("close", () => { - console.log(dim("[voice] Browser disconnected")); - adapter?.disconnect().catch(() => {}); - // Summarize chat history, save mood, and write journal — track promises for graceful shutdown - const p = Promise.allSettled([ - summarizeHistory(opts.agentDir, activeBranch).catch((err) => { - console.error(dim(`[voice] Background summarization failed: ${err.message}`)); - }), - saveMoodEntry(opts.agentDir, moodCounts, sessionMessageCount).catch((err) => { - console.error(dim(`[voice] Mood save failed: ${err.message}`)); - }), - writeJournalEntry(opts.agentDir, activeBranch, moodCounts, opts.model, opts.env).catch((err) => { - console.error(dim(`[voice] Journal write failed: ${err.message}`)); - }), - ]); - pendingShutdownWork.push(p); - }); - }); - - await new Promise((resolve) => { - httpServer.listen(port, () => resolve()); - }); - - console.log(bold(`Voice server running on :${port}`)); - console.log(dim(`[voice] Backend: ${opts.adapter}`)); - console.log(dim(`[voice] Agent dir: ${agentRoot}`)); - console.log(dim(`[voice] Model: ${opts.model || "(default)"}`)); - console.log(dim(`[voice] Composio: ${composioAdapter ? "enabled" : "disabled"}`)); - console.log(dim(`[voice] Telegram: ${telegramToken ? "configured" : "not configured"}`)); - console.log(dim(`[voice] Auth: ${serverPassword ? `protected (user "${serverUsername}")` : "open — set GITAGENT_PASSWORD (and optionally GITAGENT_USERNAME) to require login"}`)); - console.log(dim(`[voice] Open http://localhost:${port} in your browser`)); - - // Start the cron scheduler - startScheduler(schedulerOpts).catch((err) => console.error(dim(`[scheduler] Init error: ${err.message}`))); - - return async () => { - // Stop scheduled jobs - stopScheduler(); - // Stop Telegram polling - stopTelegramPolling(); - // Gracefully close WebSocket connections to trigger close handlers (journal, mood, etc.) - for (const client of wss.clients) { - client.close(1000, "Server shutting down"); - } - // Wait for close handlers to fire, then await their async work (journal writes, etc.) - await new Promise((r) => setTimeout(r, 200)); - if (pendingShutdownWork.length > 0) { - console.log(dim("[voice] Waiting for journal & mood saves...")); - await Promise.allSettled(pendingShutdownWork); - } - wss.close(); - await new Promise((resolve) => { - httpServer.close(() => resolve()); - }); - console.log(dim("[voice] Server stopped")); - }; -} - -function safeSend(ws: WS, data: string) { - if (ws.readyState === WS.OPEN) { - ws.send(data); - } -} diff --git a/src/voice/ui.html b/src/voice/ui.html deleted file mode 100644 index b81ba84..0000000 --- a/src/voice/ui.html +++ /dev/null @@ -1,3859 +0,0 @@ - - - - - -Gitagent: {{AGENT_NAME}} - - - -
-
- -

Gitagent: {{AGENT_NAME}}

- -
- - - - - - - - - -
-
-
- - - Disconnected -
-
-
- - -
- -
-
-
Camera off
- - -
-
- - - - - -
-
-
- Agent Vitals - -
-
-
- CPU - 0% -
-
-
- Memory - 0MB -
-
-
- Tokens - 0tok -
-
-
- Uptime - 00:00 -
-
-
- Pulse - -
-
-
-
-
-
-
-
-
- - - - -
-
Drop files here
-
-
-
-
- Files - -
-
-
-
-
-
- - VIEWING -
- - - -
-
-
-

-          
-          
-          
-          
-          
-          
-          
-        
-
-
-
- - - - - - - - -
- - - - - -