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
-
-
-
GitAgent
-
Sign in to continue
-
Incorrect username or password
-
-
-`;
-
- // 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(`
-