diff --git a/.claude/skills/claude-remote-codebase/SKILL.md b/.claude/skills/claude-remote-codebase/SKILL.md new file mode 100644 index 0000000..8bc4bcb --- /dev/null +++ b/.claude/skills/claude-remote-codebase/SKILL.md @@ -0,0 +1,54 @@ +--- +name: claude-remote-codebase +description: "Use when looking up Claude-Code-Remote setup, Telegram bot configuration, ngrok webhook, or token security model. Triggers on \"telegram bot\", \"ngrok setup\", \"webhook URL\", \"remote control\", \"token security\". Do NOT use for general Claude Code or Agent SDK usage." +metadata: + skill-type: codebase + scope: project + version: 1.0.0 +--- + +# Claude-Code-Remote Reference + +## Quick Start + +Every time you want to use Telegram to control Claude: + +```bash +# 1. Start tmux with Claude +tmux new-session -s default +claude +# (Detach later: Ctrl+B, then D) + +# 2. In another terminal, start the services +cd /Users/tpellet/Projects/Claude-Code-Remote +ngrok http 3001 & +npm run telegram +``` + +## If ngrok URL Changed + +ngrok generates a new URL on every restart. Update the webhook: + +```bash +# Get new URL +curl -s http://localhost:4040/api/tunnels | grep -o 'https://[^"]*' + +# Set it (replace YOUR-NEW-NGROK-URL) +curl -X POST "https://api.telegram.org/bot$(grep TELEGRAM_BOT_TOKEN .env | cut -d= -f2)/setWebhook" \ + -d "url=https://YOUR-NEW-NGROK-URL/webhook/telegram" +``` + +## Usage + +1. Claude finishes a task → Telegram notification with token +2. Reply: `/cmd TOKEN123 your next instruction` +3. Command runs in Claude + +## Token Security + +- Only you can send commands (tied to your chat ID + token) +- Each token is linked to a specific Claude session +- Tokens expire after 24 hours +- Prevents unauthorized command injection + +**Tip**: Pay for ngrok ($8/mo) for a static URL — skip the webhook update step. diff --git a/package-lock.json b/package-lock.json index ec0a450..4473c40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,12 @@ "node": ">=14.0.0" } }, + "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/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmmirror.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -59,6 +65,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@zone-eu/mailsplit": { + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.8.tgz", + "integrity": "sha512-eEyACj4JZ7sjzRvy26QhLgKEMWwQbsw1+QZnlLX+/gihcNH07lVPOcnwf5U6UAL7gkc//J3jVd76o/WS+taUiA==", + "license": "(MIT OR EUPL-1.1+)", + "dependencies": { + "libbase64": "1.3.0", + "libmime": "5.3.7", + "libqp": "2.1.1" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -93,9 +110,9 @@ } }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -104,29 +121,49 @@ } }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/body-parser/node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -139,6 +176,15 @@ "node": ">=0.10.0" } }, + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -500,39 +546,39 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -843,19 +889,67 @@ } }, "node_modules/imapflow": { - "version": "1.0.191", - "resolved": "https://registry.npmmirror.com/imapflow/-/imapflow-1.0.191.tgz", - "integrity": "sha512-cjGj5RYOZe6L/B1sn/yaSK0HJt/6OH5KVvuZ4wi4eJwm+3DUZT5rFljAqJH7Nc/UlOqdeCH2Tm47/Aa5apOkrg==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/imapflow/-/imapflow-1.2.6.tgz", + "integrity": "sha512-mzXKNXR62HPZJKHrdV7jzbvS3taJD/5XFErVmyBGVBNKaGxx4/tDrdCpcpJl25JhgzMnibhhH5/klQq0W2obrA==", + "license": "MIT", "dependencies": { + "@zone-eu/mailsplit": "5.4.8", "encoding-japanese": "2.2.0", - "iconv-lite": "0.6.3", + "iconv-lite": "0.7.1", "libbase64": "1.3.0", "libmime": "5.3.7", "libqp": "2.1.1", - "mailsplit": "5.4.5", - "nodemailer": "7.0.5", - "pino": "9.7.0", - "socks": "2.8.5" + "nodemailer": "7.0.12", + "pino": "10.1.0", + "socks": "2.8.7" + } + }, + "node_modules/imapflow/node_modules/iconv-lite": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/imapflow/node_modules/nodemailer": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.12.tgz", + "integrity": "sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/imapflow/node_modules/pino": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz", + "integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==", + "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/inherits": { @@ -864,13 +958,10 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", "engines": { "node": ">= 12" } @@ -930,11 +1021,6 @@ "node": ">=10" } }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" - }, "node_modules/leac": { "version": "0.6.0", "resolved": "https://registry.npmmirror.com/leac/-/leac-0.6.0.tgz", @@ -973,38 +1059,37 @@ } }, "node_modules/mailparser": { - "version": "3.7.4", - "resolved": "https://registry.npmmirror.com/mailparser/-/mailparser-3.7.4.tgz", - "integrity": "sha512-Beh4yyR4jLq3CZZ32asajByrXnW8dLyKCAQD3WvtTiBnMtFWhxO+wa93F6sJNjDmfjxXs4NRNjw3XAGLqZR3Vg==", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.9.1.tgz", + "integrity": "sha512-6vHZcco3fWsDMkf4Vz9iAfxvwrKNGbHx0dV1RKVphQ/zaNY34Buc7D37LSa09jeSeybWzYcTPjhiZFxzVRJedA==", + "license": "MIT", "dependencies": { + "@zone-eu/mailsplit": "5.4.8", "encoding-japanese": "2.2.0", "he": "1.2.0", "html-to-text": "9.0.5", - "iconv-lite": "0.6.3", + "iconv-lite": "0.7.0", "libmime": "5.3.7", "linkify-it": "5.0.0", - "mailsplit": "5.4.5", - "nodemailer": "7.0.4", + "nodemailer": "7.0.11", "punycode.js": "2.3.1", - "tlds": "1.259.0" - } - }, - "node_modules/mailparser/node_modules/nodemailer": { - "version": "7.0.4", - "resolved": "https://registry.npmmirror.com/nodemailer/-/nodemailer-7.0.4.tgz", - "integrity": "sha512-9O00Vh89/Ld2EcVCqJ/etd7u20UhME0f/NToPfArwPEe1Don1zy4mAIz6ariRr7mJ2RDxtaDzN0WJVdVXPtZaw==", - "engines": { - "node": ">=6.0.0" + "tlds": "1.261.0" } }, - "node_modules/mailsplit": { - "version": "5.4.5", - "resolved": "https://registry.npmmirror.com/mailsplit/-/mailsplit-5.4.5.tgz", - "integrity": "sha512-oMfhmvclR689IIaQmIcR5nODnZRRVwAKtqFT407TIvmhX2OLUBnshUTcxzQBt3+96sZVDud9NfSe1NxAkUNXEQ==", + "node_modules/mailparser/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", "dependencies": { - "libbase64": "1.3.0", - "libmime": "5.3.7", - "libqp": "2.1.1" + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/math-intrinsics": { @@ -1126,9 +1211,10 @@ } }, "node_modules/nodemailer": { - "version": "7.0.5", - "resolved": "https://registry.npmmirror.com/nodemailer/-/nodemailer-7.0.5.tgz", - "integrity": "sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==", + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.11.tgz", + "integrity": "sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==", + "license": "MIT-0", "engines": { "node": ">=6.0.0" } @@ -1376,12 +1462,12 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -1405,20 +1491,40 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/raw-body/node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1431,6 +1537,15 @@ "node": ">=0.10.0" } }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", @@ -1672,19 +1787,21 @@ }, "node_modules/smart-buffer": { "version": "4.2.0", - "resolved": "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" } }, "node_modules/socks": { - "version": "2.8.5", - "resolved": "https://registry.npmmirror.com/socks/-/socks-2.8.5.tgz", - "integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==", + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", "dependencies": { - "ip-address": "^9.0.5", + "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -1708,11 +1825,6 @@ "node": ">= 10.x" } }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" - }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -1761,9 +1873,10 @@ } }, "node_modules/tlds": { - "version": "1.259.0", - "resolved": "https://registry.npmmirror.com/tlds/-/tlds-1.259.0.tgz", - "integrity": "sha512-AldGGlDP0PNgwppe2quAvuBl18UcjuNtOnDuUkqhd6ipPqrYYBt3aTxK1QTsBVknk97lS2JcafWMghjGWFtunw==", + "version": "1.261.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz", + "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", + "license": "MIT", "bin": { "tlds": "bin.js" } diff --git a/src/channels/telegram/telegram.js b/src/channels/telegram/telegram.js index fc1cc4e..b1b5ae8 100644 --- a/src/channels/telegram/telegram.js +++ b/src/channels/telegram/telegram.js @@ -18,7 +18,26 @@ class TelegramChannel extends NotificationChannel { this.tmuxMonitor = new TmuxMonitor(); this.apiBaseUrl = 'https://api.telegram.org'; this.botUsername = null; // Cache for bot username - + + // Telegram limits: 4096 chars per message, leave room for formatting + this.MAX_MESSAGE_LENGTH = 3800; + this.MAX_QUESTION_LENGTH = 500; + this.MAX_RESPONSE_LENGTH = 2500; + + // Characters to strip from terminal output (box drawing, lines, etc.) + this.terminalJunkPattern = /[─━┄┅┈┉╌╍═╭╮╯╰│┃┆┇┊┋╎╏║╒╓╔╕╖╗╘╙╚╛╜╝╞╟╠╡╢╣╤╥╦╧╨╩╪╫╬▀▁▂▃▄▅▆▇█▉▊▋▌▍▎▏▐░▒▓⏺■□▪▫●○◆◇◈★☆✓✔✕✖✗✘⚠️➜➤►▶▷◀◁←→↑↓↔↕⬆⬇⬅➡↩↪⤴⤵🔑📁📝🤖💬⏳✅❌⏭️🚫⚠️_]+/g; + + // Patterns that indicate an approval/permission request + this.approvalPatterns = [ + /\b(do you want|would you like|shall I|should I|can I|may I)\b.*\?/i, + /\b(proceed|continue|confirm|approve|allow|permit|accept)\b.*\?/i, + /\b(yes|no|y\/n)\b.*\?/i, + /\b(permission|approval|authorization)\b/i, + /\b(run|execute|delete|remove|modify|change|update|create|write)\b.*\?/i, + /\ballow.*tool\b/i, + /\baccept.*edit\b/i, + ]; + this._ensureDirectories(); this._validateConfig(); } @@ -63,6 +82,129 @@ class TelegramChannel extends NotificationChannel { return token; } + /** + * Sanitize terminal output by removing box-drawing chars and cleaning up + */ + _sanitizeTerminalOutput(text) { + if (!text) return ''; + return text + // Remove box-drawing and line characters + .replace(/[─━┄┅┈┉╌╍═╭╮╯╰│┃┆┇┊┋╎╏║]+/g, '') + // Remove lines of underscores or dashes + .replace(/^[_\-─━=]{3,}$/gm, '') + // Remove ANSI escape codes + .replace(/\x1b\[[0-9;]*m/g, '') + // Collapse multiple blank lines into one + .replace(/\n{3,}/g, '\n\n') + // Collapse multiple spaces + .replace(/ +/g, ' ') + // Trim each line + .split('\n').map(l => l.trim()).filter(l => l).join('\n') + .trim(); + } + + /** + * Detect if the response contains an approval/permission request + * @param {string} text - The text to analyze + * @returns {Object|null} - Approval info with extracted question, or null + */ + _detectApprovalRequest(text) { + if (!text) return null; + + for (const pattern of this.approvalPatterns) { + const match = text.match(pattern); + if (match) { + // Extract the sentence containing the approval request + const sentences = text.split(/(?<=[.!?])\s+/); + const approvalSentence = sentences.find(s => pattern.test(s)) || match[0]; + + return { + detected: true, + question: approvalSentence.trim(), + fullMatch: match[0] + }; + } + } + return null; + } + + /** + * Generate inline keyboard buttons for approval requests + * @param {string} token - Session token + * @returns {Array} - Inline keyboard button rows + */ + _generateApprovalButtons(token) { + return [ + [ + { text: '✅ Yes', callback_data: `approve:${token}:yes` }, + { text: '❌ No', callback_data: `approve:${token}:no` } + ], + [ + { text: '⏭️ Skip', callback_data: `approve:${token}:skip` }, + { text: '🚫 Always Deny', callback_data: `approve:${token}:always_deny` } + ], + [ + { text: '✅ Yes, don\'t ask again', callback_data: `approve:${token}:always_allow` } + ] + ]; + } + + /** + * Split a long message into multiple parts + * @param {string} text - Text to split + * @param {number} maxLength - Maximum length per part + * @returns {Array} - Array of message parts + */ + _splitMessage(text, maxLength = this.MAX_MESSAGE_LENGTH) { + if (text.length <= maxLength) { + return [text]; + } + + const parts = []; + let remaining = text; + let partNum = 1; + + while (remaining.length > 0) { + let chunk; + if (remaining.length <= maxLength) { + chunk = remaining; + remaining = ''; + } else { + // Find a good break point (paragraph, sentence, or word boundary) + let breakPoint = remaining.lastIndexOf('\n\n', maxLength); + if (breakPoint < maxLength * 0.5) { + breakPoint = remaining.lastIndexOf('\n', maxLength); + } + if (breakPoint < maxLength * 0.5) { + breakPoint = remaining.lastIndexOf('. ', maxLength); + } + if (breakPoint < maxLength * 0.5) { + breakPoint = remaining.lastIndexOf(' ', maxLength); + } + if (breakPoint < maxLength * 0.3) { + breakPoint = maxLength; + } + + chunk = remaining.substring(0, breakPoint + 1).trim(); + remaining = remaining.substring(breakPoint + 1).trim(); + } + + if (chunk) { + parts.push(chunk); + partNum++; + } + } + + // Add part indicators if multiple parts + if (parts.length > 1) { + return parts.map((part, i) => + `📄 *[${i + 1}/${parts.length}]*\n\n${part}` + ); + } + + return parts; + } + _getCurrentTmuxSession() { try { // Try to get current tmux session @@ -124,45 +266,54 @@ class TelegramChannel extends NotificationChannel { // Create session record await this._createSession(sessionId, notification, token); - // Generate Telegram message - const messageText = this._generateTelegramMessage(notification, sessionId, token); - + // Generate Telegram message with approval detection + const messageResult = this._generateTelegramMessage(notification, sessionId, token); + // Determine recipient (chat or group) const chatId = this.config.groupId || this.config.chatId; - const isGroupChat = !!this.config.groupId; - - // Create buttons using callback_data instead of inline query - // This avoids the automatic @bot_name addition - const buttons = [ - [ - { - text: '📝 Personal Chat', - callback_data: `personal:${token}` - }, - { - text: '👥 Group Chat', - callback_data: `group:${token}` - } - ] - ]; - - const requestData = { - chat_id: chatId, - text: messageText, - parse_mode: 'Markdown', - reply_markup: { - inline_keyboard: buttons - } - }; + + // Choose buttons based on whether there's an approval request + let buttons; + if (messageResult.hasApproval && messageResult.approvalButtons) { + // Use approval buttons when permission is needed + buttons = messageResult.approvalButtons; + } else { + // Default help buttons + buttons = [ + [ + { text: '📝 Personal Chat', callback_data: `personal:${token}` }, + { text: '👥 Group Chat', callback_data: `group:${token}` } + ] + ]; + } try { - const response = await axios.post( - `${this.apiBaseUrl}/bot${this.config.botToken}/sendMessage`, - requestData, - this._getNetworkOptions() - ); + // Split message if too long + const messageParts = this._splitMessage(messageResult.text); + + for (let i = 0; i < messageParts.length; i++) { + const isLastPart = i === messageParts.length - 1; + const requestData = { + chat_id: chatId, + text: messageParts[i], + parse_mode: 'Markdown', + // Only add buttons to the last message + ...(isLastPart && { reply_markup: { inline_keyboard: buttons } }) + }; + + await axios.post( + `${this.apiBaseUrl}/bot${this.config.botToken}/sendMessage`, + requestData, + this._getNetworkOptions() + ); - this.logger.info(`Telegram message sent successfully, Session: ${sessionId}`); + // Small delay between messages to maintain order + if (!isLastPart) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + } + + this.logger.info(`Telegram message sent successfully, Session: ${sessionId}, Parts: ${messageParts.length}`); return true; } catch (error) { this.logger.error('Failed to send Telegram message:', error.response?.data || error.message); @@ -172,38 +323,45 @@ class TelegramChannel extends NotificationChannel { } } + /** + * Generate Telegram message with approval detection and improved formatting + * @param {Object} notification - Notification object + * @param {string} sessionId - Session UUID + * @param {string} token - Session token + * @returns {Object} - { text, hasApproval, approvalButtons } + */ _generateTelegramMessage(notification, sessionId, token) { const type = notification.type; const emoji = type === 'completed' ? '✅' : '⏳'; const status = type === 'completed' ? 'Completed' : 'Waiting for Input'; - - let messageText = `${emoji} *Claude Task ${status}*\n`; - messageText += `*Project:* ${notification.project}\n`; - messageText += `*Session Token:* \`${token}\`\n\n`; - - if (notification.metadata) { - if (notification.metadata.userQuestion) { - messageText += `📝 *Your Question:*\n${notification.metadata.userQuestion.substring(0, 200)}`; - if (notification.metadata.userQuestion.length > 200) { - messageText += '...'; - } - messageText += '\n\n'; - } - - if (notification.metadata.claudeResponse) { - messageText += `🤖 *Claude Response:*\n${notification.metadata.claudeResponse.substring(0, 300)}`; - if (notification.metadata.claudeResponse.length > 300) { - messageText += '...'; - } - messageText += '\n\n'; - } + + const claudeResponse = notification.metadata?.claudeResponse || ''; + const approvalInfo = this._detectApprovalRequest(claudeResponse); + + // Sanitize the response to remove terminal UI artifacts + const cleanResponse = this._sanitizeTerminalOutput(claudeResponse); + const cleanApproval = approvalInfo ? this._sanitizeTerminalOutput(approvalInfo.question) : null; + + // Ultra-compact format + let messageText = `${emoji} \`${token}\` ${notification.project}`; + + if (cleanApproval) { + messageText += `\n⚠️ ${cleanApproval}`; + } else if (cleanResponse) { + const maxLen = this.MAX_RESPONSE_LENGTH; + const truncated = cleanResponse.length > maxLen; + messageText += `\n${cleanResponse.substring(0, maxLen)}${truncated ? '...' : ''}`; + } + + if (!approvalInfo) { + messageText += `\n\n/cmd ${token} ...`; } - - messageText += `💬 *To send a new command:*\n`; - messageText += `Reply with: \`/cmd ${token} \`\n`; - messageText += `Example: \`/cmd ${token} Please analyze this code\``; - return messageText; + return { + text: messageText, + hasApproval: !!approvalInfo, + approvalButtons: approvalInfo ? this._generateApprovalButtons(token) : null + }; } async _createSession(sessionId, notification, token) { diff --git a/src/channels/telegram/webhook.js b/src/channels/telegram/webhook.js index aca77c0..5449228 100644 --- a/src/channels/telegram/webhook.js +++ b/src/channels/telegram/webhook.js @@ -160,32 +160,107 @@ class TelegramWebhookHandler { async _handleCallbackQuery(callbackQuery) { const chatId = callbackQuery.message.chat.id; const data = callbackQuery.data; - + // Answer callback query to remove loading state await this._answerCallbackQuery(callbackQuery.id); - - if (data.startsWith('personal:')) { + + if (data.startsWith('approve:')) { + // Handle approval button clicks: approve:TOKEN:action + await this._handleApprovalCallback(chatId, data); + } else if (data.startsWith('personal:')) { const token = data.split(':')[1]; - // Send personal chat command format await this._sendMessage(chatId, `📝 *Personal Chat Command Format:*\n\n\`/cmd ${token} \`\n\n*Example:*\n\`/cmd ${token} please analyze this code\`\n\n💡 *Copy and paste the format above, then add your command!*`, { parse_mode: 'Markdown' }); } else if (data.startsWith('group:')) { const token = data.split(':')[1]; - // Send group chat command format with @bot_name const botUsername = await this._getBotUsername(); await this._sendMessage(chatId, `👥 *Group Chat Command Format:*\n\n\`@${botUsername} /cmd ${token} \`\n\n*Example:*\n\`@${botUsername} /cmd ${token} please analyze this code\`\n\n💡 *Copy and paste the format above, then add your command!*`, { parse_mode: 'Markdown' }); } else if (data.startsWith('session:')) { const token = data.split(':')[1]; - // For backward compatibility - send help message for old callback buttons await this._sendMessage(chatId, `📝 *How to send a command:*\n\nType:\n\`/cmd ${token} \`\n\nExample:\n\`/cmd ${token} please analyze this code\`\n\n💡 *Tip:* New notifications have a button that auto-fills the command for you!`, { parse_mode: 'Markdown' }); } } + /** + * Handle approval button callbacks + * @param {number} chatId - Telegram chat ID + * @param {string} data - Callback data in format approve:TOKEN:action + */ + async _handleApprovalCallback(chatId, data) { + const parts = data.split(':'); + if (parts.length !== 3) { + await this._sendMessage(chatId, '❌ Invalid approval callback format.'); + return; + } + + const token = parts[1]; + const action = parts[2]; + + // Find session by token + const session = await this._findSessionByToken(token); + if (!session) { + await this._sendMessage(chatId, + '❌ Session expired or invalid. Please wait for a new notification.', + { parse_mode: 'Markdown' }); + return; + } + + // Check if session is expired + if (session.expiresAt < Math.floor(Date.now() / 1000)) { + await this._sendMessage(chatId, + '❌ Token has expired. Please wait for a new task notification.', + { parse_mode: 'Markdown' }); + await this._removeSession(session.id); + return; + } + + // Map button actions to Claude responses + const actionMap = { + 'yes': 'yes', + 'no': 'no', + 'skip': 'skip', + 'always_allow': 'yes, always allow this', + 'always_deny': 'no, always deny this' + }; + + const command = actionMap[action]; + if (!command) { + await this._sendMessage(chatId, '❌ Unknown action.'); + return; + } + + const actionEmoji = { + 'yes': '✅', + 'no': '❌', + 'skip': '⏭️', + 'always_allow': '✅🔓', + 'always_deny': '🚫' + }; + + try { + // Inject the approval response into tmux session + const tmuxSession = session.tmuxSession || 'default'; + await this.injector.injectCommand(command, tmuxSession); + + await this._sendMessage(chatId, + `${actionEmoji[action]} *Response sent:* \`${command}\`\n\n🖥️ *Session:* ${tmuxSession}\n\nClaude is processing your response...`, + { parse_mode: 'Markdown' }); + + this.logger.info(`Approval response - Token: ${token}, Action: ${action}, Command: ${command}`); + + } catch (error) { + this.logger.error('Approval injection failed:', error.message); + await this._sendMessage(chatId, + `❌ *Failed to send response:* ${error.message}`, + { parse_mode: 'Markdown' }); + } + } + async _sendWelcomeMessage(chatId) { const message = `🤖 *Welcome to Claude Code Remote Bot!*\n\n` + `I'll notify you when Claude completes tasks or needs input.\n\n` + diff --git a/src/data/session-map.json b/src/data/session-map.json index 2ca03b0..0967ef4 100644 --- a/src/data/session-map.json +++ b/src/data/session-map.json @@ -1,542 +1 @@ -{ - "7HUMGXOT": { - "type": "pty", - "createdAt": 1753601264, - "expiresAt": 1753687664, - "cwd": "/Users/jessytsui/dev/TaskPing", - "sessionId": "3248adc2-eb7b-4eb2-a57c-b9ce320cb4ec", - "tmuxSession": "hailuo", - "description": "completed - TaskPing" - }, - "5CLDW6NQ": { - "type": "pty", - "createdAt": 1753602124, - "expiresAt": 1753688524, - "cwd": "/Users/jessytsui/dev/TaskPing", - "sessionId": "c7b6750f-6246-4ed3-bca5-81201ab980ee", - "tmuxSession": "hailuo", - "description": "completed - TaskPing" - }, - "ONY66DAE": { - "type": "pty", - "createdAt": 1753604311, - "expiresAt": 1753690711, - "cwd": "/Users/jessytsui/dev/TaskPing", - "sessionId": "2a8622dd-a0e9-4f4d-9cc8-a3bb432e9621", - "tmuxSession": "hailuo", - "description": "waiting - TaskPing" - }, - "QHFI9FIJ": { - "type": "pty", - "createdAt": 1753607753, - "expiresAt": 1753694153, - "cwd": "/Users/jessytsui/dev/TaskPing", - "sessionId": "a966681d-5cfd-47b9-bb1b-c7ee9655b97b", - "tmuxSession": "a-0", - "description": "waiting - TaskPing" - }, - "G3QE3STQ": { - "type": "pty", - "createdAt": 1753622403, - "expiresAt": 1753708803, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "7f6e11b3-0ac9-44b1-a75f-d3a15a8ec46e", - "tmuxSession": "claude-taskping", - "description": "completed - TaskPing-Test" - }, - "Z0Q98XCC": { - "type": "pty", - "createdAt": 1753624374, - "expiresAt": 1753710774, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "b2408839-8a64-4a07-8ceb-4a7a82ea5b25", - "tmuxSession": "claude-taskping", - "description": "completed - TaskPing-Test" - }, - "65S5UGHZ": { - "type": "pty", - "createdAt": 1753624496, - "expiresAt": 1753710896, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "117a1097-dd97-41ab-a276-e4820adc8da8", - "tmuxSession": "claude-taskping", - "description": "completed - Claude-Code-Remote" - }, - "N9PPKGTO": { - "type": "pty", - "createdAt": 1753624605, - "expiresAt": 1753711005, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "0caed3af-35ae-4b42-9081-b1a959735bde", - "tmuxSession": "video", - "description": "completed - Claude-Code-Remote" - }, - "TEST12345": { - "type": "pty", - "createdAt": 1753628000, - "expiresAt": 1753714400, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "test-session-id", - "tmuxSession": "test-claude", - "description": "testing - Test Session" - }, - "YTGT6F6F": { - "type": "pty", - "createdAt": 1753625040, - "expiresAt": 1753711440, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "e6e973b6-20dd-497f-a988-02482af63336", - "tmuxSession": "claude-taskping", - "description": "completed - Claude-Code-Remote" - }, - "2XQP1N0P": { - "type": "pty", - "createdAt": 1753625361, - "expiresAt": 1753711761, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "0f694f4c-f8a4-476a-946a-3dc057c3bc46", - "tmuxSession": "video", - "description": "completed - Claude-Code-Remote" - }, - "GKPSGCBS": { - "type": "pty", - "createdAt": 1753625618, - "expiresAt": 1753712018, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "e844d2ae-9098-4528-9e05-e77904a35be3", - "tmuxSession": "claude-taskping", - "description": "completed - Claude-Code-Remote" - }, - "187JDGZ0": { - "type": "pty", - "createdAt": 1753625623, - "expiresAt": 1753712023, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "633a2687-81e7-456e-9995-3321ce3f3b2b", - "tmuxSession": "video", - "description": "completed - Claude-Code-Remote" - }, - "NSYKTAWC": { - "type": "pty", - "createdAt": 1753625650, - "expiresAt": 1753712050, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "b8dac307-8b4b-4286-aa73-324b9b659e60", - "tmuxSession": "claude-taskping", - "description": "completed - Claude-Code-Remote" - }, - "1NTEJPH7": { - "type": "pty", - "createdAt": 1753625743, - "expiresAt": 1753712143, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "09466a43-c495-4a30-ac08-eb425748a28c", - "tmuxSession": "claude-taskping", - "description": "completed - Claude-Code-Remote" - }, - "5XO64F9Z": { - "type": "pty", - "createdAt": 1753625846, - "expiresAt": 1753712246, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "99132172-7a97-46f7-b282-b22054d6e599", - "tmuxSession": "video", - "description": "completed - Claude-Code-Remote" - }, - "D8561S3A": { - "type": "pty", - "createdAt": 1753625904, - "expiresAt": 1753712304, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "772628f1-414b-4242-bc8f-660ad53b6c23", - "tmuxSession": "video", - "description": "completed - Claude-Code-Remote" - }, - "GR0GED2E": { - "type": "pty", - "createdAt": 1753626215, - "expiresAt": 1753712615, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "da40ba76-7047-41e0-95f2-081db87c1b3b", - "tmuxSession": "video", - "description": "completed - Claude-Code-Remote" - }, - "TTRQKVM9": { - "type": "pty", - "createdAt": 1753626245, - "expiresAt": 1753712645, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "c7c5c95d-4541-47f6-b27a-35c0fd563413", - "tmuxSession": "video", - "description": "completed - Claude-Code-Remote" - }, - "P9UBHY8L": { - "type": "pty", - "createdAt": 1753626325, - "expiresAt": 1753712725, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "a3f2b4f9-811e-4721-914f-f025919c2530", - "tmuxSession": "video", - "description": "completed - Claude-Code-Remote" - }, - "JQAOXCYJ": { - "type": "pty", - "createdAt": 1753626390, - "expiresAt": 1753712790, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "f0d0635b-59f2-45eb-acfc-d649b12fd2d6", - "tmuxSession": "video", - "description": "completed - Claude-Code-Remote" - }, - "B7R9OR3K": { - "type": "pty", - "createdAt": 1753626445, - "expiresAt": 1753712845, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "d33e49aa-a58f-46b0-8829-dfef7f474600", - "tmuxSession": "video", - "description": "completed - Claude-Code-Remote" - }, - "0KGM60XO": { - "type": "pty", - "createdAt": 1753626569, - "expiresAt": 1753712969, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "02bd4449-bdcf-464e-916e-61bc62a18dd2", - "tmuxSession": "video", - "description": "completed - Claude-Code-Remote" - }, - "5NXM173C": { - "type": "pty", - "createdAt": 1753626834, - "expiresAt": 1753713234, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "f8f915ee-ab64-471c-b3d2-71cb84d2b5fe", - "tmuxSession": "video", - "description": "completed - Claude-Code-Remote" - }, - "2R8GD6VD": { - "type": "pty", - "createdAt": 1754066705, - "expiresAt": 1754153105, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "00a65f0d-b8ef-4c7f-97d6-74ace997f133", - "tmuxSession": "a", - "description": "completed - Claude-Code-Remote" - }, - "01BCXOI0": { - "type": "pty", - "createdAt": 1754066792, - "expiresAt": 1754153192, - "cwd": "/Users/jessytsui/dev/doc_page", - "sessionId": "47501ac6-1ae1-4584-9339-e64ebe8f4218", - "tmuxSession": "claude-test", - "description": "completed - doc_page" - }, - "SLHHY01G": { - "type": "pty", - "createdAt": 1754066895, - "expiresAt": 1754153295, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "2704564c-3c4f-4174-95fb-33e476acd44a", - "tmuxSession": "a", - "description": "completed - Claude-Code-Remote" - }, - "9CUAHQ60": { - "type": "pty", - "createdAt": 1754066975, - "expiresAt": 1754153375, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "ba41d320-6579-4beb-bb62-0ea3cdeacfcb", - "tmuxSession": "a", - "description": "completed - Claude-Code-Remote" - }, - "C8ZKMS70": { - "type": "pty", - "createdAt": 1754067144, - "expiresAt": 1754153544, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "3050f159-e58f-4a3a-b744-45ae38f2e887", - "tmuxSession": "a", - "description": "completed - Claude-Code-Remote" - }, - "E71FAJA3": { - "type": "pty", - "createdAt": 1754067280, - "expiresAt": 1754153680, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "784bb250-9f56-4511-836f-a38f76c486ce", - "tmuxSession": "a", - "description": "completed - Claude-Code-Remote" - }, - "ZQIZ0SDR": { - "type": "pty", - "createdAt": 1754067317, - "expiresAt": 1754153717, - "cwd": "/Users/jessytsui/dev/doc_page", - "sessionId": "7277653b-cae9-4be9-9124-bc68a71c7152", - "tmuxSession": "claude-test", - "description": "completed - doc_page" - }, - "0IEID7K0": { - "type": "pty", - "createdAt": 1754067385, - "expiresAt": 1754153785, - "cwd": "/Users/jessytsui", - "sessionId": "73e4e49d-bd47-43f8-92dc-42e530675a0a", - "tmuxSession": "claude-test", - "description": "completed - jessytsui" - }, - "2MFHRVRP": { - "type": "pty", - "createdAt": 1754067582, - "expiresAt": 1754153982, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "60ed38da-6940-425e-a9fe-491840a3e0e7", - "tmuxSession": "a", - "description": "completed - Claude-Code-Remote" - }, - "WQUR8ZWG": { - "type": "pty", - "createdAt": 1754067778, - "expiresAt": 1754154178, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "bea5317e-5851-4d4a-9175-b79f766bc8a0", - "tmuxSession": "claude-code-remote", - "description": "completed - Claude-Code-Remote" - }, - "VGMHY9GU": { - "type": "pty", - "createdAt": 1754067874, - "expiresAt": 1754154274, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "2c4a832b-17e2-4005-af2b-f1452315269e", - "tmuxSession": "claude-code", - "description": "completed - Claude-Code-Remote" - }, - "EMECQ2ZG": { - "type": "pty", - "createdAt": 1754067996, - "expiresAt": 1754154396, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "e61867fb-9199-4fd4-8396-f41eac9cd9af", - "tmuxSession": "claude-code", - "description": "completed - Claude-Code-Remote" - }, - "WZKOH82S": { - "type": "pty", - "createdAt": 1754068559, - "expiresAt": 1754154959, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "ef2a8e7d-c2e0-4329-9d30-998c23ad9149", - "tmuxSession": "claude-code", - "description": "completed - Claude-Code-Remote" - }, - "O3ST8AI6": { - "type": "pty", - "createdAt": 1754069309, - "expiresAt": 1754155709, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "0aec093e-f4e0-45f6-8f15-b734f56e254f", - "tmuxSession": "claude-code", - "description": "completed - Claude-Code-Remote" - }, - "DUMTN3RR": { - "type": "pty", - "createdAt": 1754069671, - "expiresAt": 1754156071, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "08333291-85f8-4672-9590-685e3049d028", - "tmuxSession": "claude-code-test", - "description": "completed - Claude-Code-Remote" - }, - "55MH1LWK": { - "type": "pty", - "createdAt": 1754070069, - "expiresAt": 1754156469, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "719bf1a7-c6b1-4d45-ad47-86c985625232", - "tmuxSession": "a", - "description": "completed - Claude-Code-Remote" - }, - "55E0J99I": { - "type": "pty", - "createdAt": 1754070135, - "expiresAt": 1754156535, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "303dc5be-af53-478f-b5cd-702987eb29b4", - "tmuxSession": "claude-code", - "description": "completed - Claude-Code-Remote" - }, - "GK0JP7C4": { - "type": "pty", - "createdAt": 1754070531, - "expiresAt": 1754156931, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "86e0c519-edf6-401b-9d44-1b980d9288f4", - "tmuxSession": "a", - "description": "completed - Claude-Code-Remote" - }, - "5NCJVV7P": { - "type": "pty", - "createdAt": 1754070572, - "expiresAt": 1754156972, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "cd0b513c-ccbc-41aa-a044-235c16083dda", - "tmuxSession": "a", - "description": "completed - Claude-Code-Remote" - }, - "DSV496RA": { - "type": "pty", - "createdAt": 1754070585, - "expiresAt": 1754156985, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "e87bb1ed-f195-407d-8024-ac8e216bc632", - "tmuxSession": "a", - "description": "completed - Claude-Code-Remote" - }, - "G4N2KB5Y": { - "type": "pty", - "createdAt": 1754070676, - "expiresAt": 1754157076, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "0c546cad-0000-4f5c-8bff-493e6dbddfe2", - "tmuxSession": "a", - "description": "completed - Claude-Code-Remote" - }, - "DIHER2N2": { - "type": "pty", - "createdAt": 1754070936, - "expiresAt": 1754157336, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "cd1cb22d-2f2f-4e4e-9ad4-e1a6c68965d0", - "tmuxSession": "a", - "description": "completed - Claude-Code-Remote" - }, - "PVTAZK0W": { - "type": "pty", - "createdAt": 1754070976, - "expiresAt": 1754157376, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "d5281021-12e4-4ca2-bc44-533649969568", - "tmuxSession": "a", - "description": "completed - Claude-Code-Remote" - }, - "6WRFBJOE": { - "type": "pty", - "createdAt": 1754071040, - "expiresAt": 1754157440, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "13dbb781-3000-4a73-9cf6-551abfcb2df8", - "tmuxSession": "a", - "description": "completed - Claude-Code-Remote" - }, - "15LBIE97": { - "type": "pty", - "createdAt": 1754071107, - "expiresAt": 1754157507, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "b1441fcc-33d5-402b-b094-c8dc4ce36302", - "tmuxSession": "a", - "description": "completed - Claude-Code-Remote" - }, - "WLPTYZ86": { - "type": "pty", - "createdAt": 1754071313, - "expiresAt": 1754157713, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "c5f99272-5603-44d3-8e97-8b19bc74d54e", - "tmuxSession": "a", - "description": "completed - Claude-Code-Remote" - }, - "QF58O43H": { - "type": "pty", - "createdAt": 1754071345, - "expiresAt": 1754157745, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "7d3bae82-97fb-42bb-bd77-af36246f47db", - "tmuxSession": "ab", - "description": "completed - Claude-Code-Remote" - }, - "U62IMVYP": { - "type": "pty", - "createdAt": 1754071829, - "expiresAt": 1754158229, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "32190311-904e-4f21-9bac-ebe458d87936", - "tmuxSession": "claude-test", - "description": "completed - Claude-Code-Remote" - }, - "L4WIBBPP": { - "type": "pty", - "createdAt": 1754074724, - "expiresAt": 1754161124, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "04bec660-6454-407c-9881-f2bc714312b0", - "tmuxSession": "123", - "description": "completed - Claude-Code-Remote" - }, - "UFORANBW": { - "type": "pty", - "createdAt": 1754074755, - "expiresAt": 1754161155, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "b75d4767-e045-4530-8740-70f6515d8b13", - "tmuxSession": "123", - "description": "completed - Claude-Code-Remote" - }, - "R0AG2CIS": { - "type": "pty", - "createdAt": 1754074784, - "expiresAt": 1754161184, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "74f9ef13-a494-44eb-83a1-6d9eacc488fa", - "tmuxSession": "123", - "description": "completed - Claude-Code-Remote" - }, - "YPBWUW83": { - "type": "pty", - "createdAt": 1754075528, - "expiresAt": 1754161928, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "c5d901bf-cb1c-4590-b197-c960c4153af2", - "tmuxSession": "claude-hook-test", - "description": "completed - Claude-Code-Remote" - }, - "8KYNDD1A": { - "type": "pty", - "createdAt": 1754075553, - "expiresAt": 1754161953, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "096b69d9-040a-4e42-a2a2-7391ba2b9e20", - "tmuxSession": "claude-hook-test", - "description": "completed - Claude-Code-Remote" - }, - "EK7LG2H4": { - "type": "pty", - "createdAt": 1754075748, - "expiresAt": 1754162148, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "94c65db1-5d64-4c10-96c3-925bb69d6bf0", - "tmuxSession": "claude-hook-test", - "description": "completed - Claude-Code-Remote" - }, - "V75VD2QD": { - "type": "pty", - "createdAt": 1754075775, - "expiresAt": 1754162175, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "74443d59-66d9-4f37-b965-dbca9d79f111", - "tmuxSession": "claude-hook-test", - "description": "completed - Claude-Code-Remote" - }, - "FJW7PHHH": { - "type": "pty", - "createdAt": 1754076112, - "expiresAt": 1754162512, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "sessionId": "10cd6e52-91a8-476a-af0a-1fe2c2929ab6", - "tmuxSession": "claude-hook-test", - "description": "completed - Claude-Code-Remote" - } -} \ No newline at end of file +{} diff --git a/test-telegram-approval-live.js b/test-telegram-approval-live.js new file mode 100644 index 0000000..1eedc4f --- /dev/null +++ b/test-telegram-approval-live.js @@ -0,0 +1,196 @@ +#!/usr/bin/env node + +/** + * Live Test: Telegram Approval Notification + * Sends an actual notification with approval buttons to Telegram + * + * Usage: node test-telegram-approval-live.js [scenario] + * + * Scenarios: + * approval - Send notification with approval request (default) + * long - Send a very long notification that gets split + * regular - Send a regular notification without approval + */ + +const path = require('path'); +const fs = require('fs'); +const dotenv = require('dotenv'); + +// Load environment variables +const envPath = path.join(__dirname, '.env'); +if (fs.existsSync(envPath)) { + dotenv.config({ path: envPath }); +} + +const TelegramChannel = require('./src/channels/telegram/telegram'); + +const scenarios = { + approval: { + type: 'waiting', + title: 'Claude needs your approval', + message: 'Claude is waiting for permission', + project: 'test-approval', + metadata: { + userQuestion: 'Please refactor the authentication module to use JWT tokens instead of sessions.', + claudeResponse: `I've analyzed the authentication module and found the following: + +1. Current implementation uses express-session +2. 15 files need to be modified +3. Tests will need updating + +**Do you want me to proceed with these changes?** + +This will: +- Replace session middleware with JWT verification +- Update all auth-related routes +- Modify the user model + +The changes are significant but reversible.`, + tmuxSession: 'test-session' + } + }, + + long: { + type: 'completed', + title: 'Analysis Complete', + message: 'Claude completed the analysis', + project: 'test-long-message', + metadata: { + userQuestion: 'Analyze the entire codebase and provide a comprehensive report.', + claudeResponse: `# Codebase Analysis Report + +## Overview +This is a comprehensive analysis of the Claude-Code-Remote project. + +## Architecture +The project follows a modular architecture with the following components: + +### Core Components +1. **Notification System** - Handles sending notifications via multiple channels +2. **Webhook Handlers** - Processes incoming messages from Telegram/LINE +3. **Session Management** - Tracks active sessions with tokens +4. **Command Injection** - Injects commands into tmux sessions + +### Channel Implementations +- **Telegram** - Full-featured with inline buttons +- **LINE** - Basic messaging support +- **Email** - SMTP-based notifications + +## File Structure +\`\`\` +src/ +├── channels/ +│ ├── telegram/ +│ ├── line/ +│ └── email/ +├── core/ +│ ├── notifier.js +│ └── logger.js +├── utils/ +│ ├── tmux-monitor.js +│ └── controller-injector.js +└── data/ + └── sessions/ +\`\`\` + +## Dependencies +- express: Web framework for webhooks +- axios: HTTP client for API calls +- node-pty: Terminal emulation +- pino: High-performance logging + +## Recommendations +1. Add rate limiting to webhook endpoints +2. Implement session cleanup cron job +3. Add health check endpoints +4. Consider adding Redis for session storage + +## Security Considerations +- Tokens expire after 24 hours +- Whitelist-based authorization +- No sensitive data in logs + +## Performance +- Average response time: <100ms +- Memory usage: ~50MB +- Supports concurrent sessions + +This analysis covers the main aspects of the codebase. Let me know if you need more details on any specific component.`, + tmuxSession: 'test-session' + } + }, + + regular: { + type: 'completed', + title: 'Task Completed', + message: 'Claude has finished the task', + project: 'test-regular', + metadata: { + userQuestion: 'What time is it?', + claudeResponse: 'Based on the system clock, the current time is 3:45 PM. Is there anything else you would like to know?', + tmuxSession: 'test-session' + } + } +}; + +async function runLiveTest(scenarioName = 'approval') { + console.log('🧪 Live Test: Telegram Notification\n'); + + if (!process.env.TELEGRAM_BOT_TOKEN || !process.env.TELEGRAM_CHAT_ID) { + console.error('❌ Error: TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID must be set in .env'); + process.exit(1); + } + + const scenario = scenarios[scenarioName]; + if (!scenario) { + console.error(`❌ Unknown scenario: ${scenarioName}`); + console.error(` Available: ${Object.keys(scenarios).join(', ')}`); + process.exit(1); + } + + console.log(`📋 Scenario: ${scenarioName}\n`); + + const config = { + botToken: process.env.TELEGRAM_BOT_TOKEN, + chatId: process.env.TELEGRAM_CHAT_ID + }; + + const telegramChannel = new TelegramChannel(config); + + try { + console.log('📱 Sending notification...\n'); + const result = await telegramChannel.send(scenario); + + if (result) { + console.log('✅ Notification sent successfully!\n'); + + if (scenarioName === 'approval') { + console.log('🎯 Expected behavior:'); + console.log(' - Message shows "ACTION REQUIRED" banner at top'); + console.log(' - Approval question is highlighted'); + console.log(' - Buttons appear: Yes, No, Skip, Always Deny, Yes don\'t ask again'); + console.log(' - Clicking a button sends the response to Claude\n'); + } else if (scenarioName === 'long') { + console.log('🎯 Expected behavior:'); + console.log(' - Message is split into multiple parts'); + console.log(' - Each part is labeled [1/N], [2/N], etc.'); + console.log(' - Help buttons appear on the last message only\n'); + } else { + console.log('🎯 Expected behavior:'); + console.log(' - Standard notification format'); + console.log(' - Personal/Group chat help buttons'); + console.log(' - No approval section\n'); + } + } else { + console.log('❌ Failed to send notification'); + process.exit(1); + } + } catch (error) { + console.error('❌ Error:', error.message); + process.exit(1); + } +} + +// Get scenario from command line +const scenario = process.argv[2] || 'approval'; +runLiveTest(scenario); diff --git a/test-telegram-approval.js b/test-telegram-approval.js new file mode 100644 index 0000000..1a8c11f --- /dev/null +++ b/test-telegram-approval.js @@ -0,0 +1,315 @@ +#!/usr/bin/env node + +/** + * Test Telegram Approval Detection and Message Formatting + * Tests the new approval detection, message splitting, and button generation features + */ + +const path = require('path'); +const fs = require('fs'); +const dotenv = require('dotenv'); + +// Load environment variables +const envPath = path.join(__dirname, '.env'); +if (fs.existsSync(envPath)) { + dotenv.config({ path: envPath }); +} + +const TelegramChannel = require('./src/channels/telegram/telegram'); + +// Test cases for approval detection +const approvalTestCases = [ + { + name: 'Permission question with "Do you want"', + text: 'I found some files. Do you want me to delete them?', + shouldDetect: true + }, + { + name: 'Permission question with "Should I"', + text: 'The tests are failing. Should I fix them automatically?', + shouldDetect: true + }, + { + name: 'Permission question with "Can I"', + text: 'Can I run npm install to add the dependencies?', + shouldDetect: true + }, + { + name: 'Permission with "proceed"', + text: 'This will modify 15 files. Proceed with the changes?', + shouldDetect: true + }, + { + name: 'Tool permission', + text: 'Claude wants to use the Bash tool. Allow tool usage?', + shouldDetect: true + }, + { + name: 'Edit acceptance', + text: 'I want to make the following changes. Accept this edit?', + shouldDetect: true + }, + { + name: 'Yes/No question', + text: 'Continue with this approach? (y/n)', + shouldDetect: true + }, + { + name: 'Regular statement (no approval)', + text: 'I have completed the analysis of your codebase.', + shouldDetect: false + }, + { + name: 'Question without approval keywords', + text: 'What framework are you using for this project?', + shouldDetect: false + }, + { + name: 'Statement with question mark', + text: 'You mentioned you want to refactor the code?', + shouldDetect: false + } +]; + +// Test cases for message splitting +const splitTestCases = [ + { + name: 'Short message (no split)', + length: 500, + expectedParts: 1 + }, + { + name: 'Medium message (no split)', + length: 3000, + expectedParts: 1 + }, + { + name: 'Long message (needs split)', + length: 5000, + expectedParts: 2 + }, + { + name: 'Very long message (multiple splits)', + length: 10000, + expectedParts: 3 + } +]; + +function generateTestText(length) { + const words = ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit']; + let text = ''; + while (text.length < length) { + text += words[Math.floor(Math.random() * words.length)] + ' '; + if (text.length % 100 < 10) text += '\n'; // Add some newlines + } + return text.substring(0, length); +} + +async function runTests() { + console.log('🧪 Testing Telegram Approval Detection and Message Formatting\n'); + console.log('=' .repeat(60) + '\n'); + + const config = { + botToken: process.env.TELEGRAM_BOT_TOKEN || 'test-token', + chatId: process.env.TELEGRAM_CHAT_ID || '12345' + }; + + const telegramChannel = new TelegramChannel(config); + + let passed = 0; + let failed = 0; + + // Test 1: Approval Detection + console.log('📋 Test 1: Approval Detection\n'); + + for (const testCase of approvalTestCases) { + const result = telegramChannel._detectApprovalRequest(testCase.text); + const detected = result !== null; + const success = detected === testCase.shouldDetect; + + if (success) { + console.log(` ✅ ${testCase.name}`); + passed++; + } else { + console.log(` ❌ ${testCase.name}`); + console.log(` Expected: ${testCase.shouldDetect ? 'approval detected' : 'no approval'}`); + console.log(` Got: ${detected ? 'approval detected' : 'no approval'}`); + if (result) { + console.log(` Matched: "${result.question}"`); + } + failed++; + } + } + + console.log('\n' + '-'.repeat(60) + '\n'); + + // Test 2: Message Splitting + console.log('📋 Test 2: Message Splitting\n'); + + for (const testCase of splitTestCases) { + const text = generateTestText(testCase.length); + const parts = telegramChannel._splitMessage(text); + const success = parts.length === testCase.expectedParts; + + if (success) { + console.log(` ✅ ${testCase.name} (${testCase.length} chars → ${parts.length} parts)`); + passed++; + } else { + console.log(` ❌ ${testCase.name}`); + console.log(` Expected: ${testCase.expectedParts} parts`); + console.log(` Got: ${parts.length} parts`); + failed++; + } + } + + console.log('\n' + '-'.repeat(60) + '\n'); + + // Test 3: Approval Buttons Generation + console.log('📋 Test 3: Approval Buttons Generation\n'); + + const buttons = telegramChannel._generateApprovalButtons('TEST1234'); + + // Check button structure + const hasYesNo = buttons[0]?.length === 2 && + buttons[0][0].text.includes('Yes') && + buttons[0][1].text.includes('No'); + const hasSkipDeny = buttons[1]?.length === 2 && + buttons[1][0].text.includes('Skip') && + buttons[1][1].text.includes('Deny'); + const hasAlwaysAllow = buttons[2]?.length === 1 && + buttons[2][0].text.includes('don\'t ask again'); + + if (hasYesNo) { + console.log(' ✅ Yes/No buttons present'); + passed++; + } else { + console.log(' ❌ Yes/No buttons missing or malformed'); + failed++; + } + + if (hasSkipDeny) { + console.log(' ✅ Skip/Always Deny buttons present'); + passed++; + } else { + console.log(' ❌ Skip/Always Deny buttons missing or malformed'); + failed++; + } + + if (hasAlwaysAllow) { + console.log(' ✅ Always Allow button present'); + passed++; + } else { + console.log(' ❌ Always Allow button missing or malformed'); + failed++; + } + + // Check callback data format + const callbackValid = buttons[0][0].callback_data === 'approve:TEST1234:yes' && + buttons[0][1].callback_data === 'approve:TEST1234:no'; + if (callbackValid) { + console.log(' ✅ Callback data format correct'); + passed++; + } else { + console.log(' ❌ Callback data format incorrect'); + console.log(` Got: ${buttons[0][0].callback_data}`); + failed++; + } + + console.log('\n' + '-'.repeat(60) + '\n'); + + // Test 4: Message Generation with Approval + console.log('📋 Test 4: Message Generation with Approval Request\n'); + + const approvalNotification = { + type: 'waiting', + project: 'test-project', + metadata: { + userQuestion: 'Please refactor this code', + claudeResponse: 'I found some issues. Do you want me to fix them automatically? This will modify several files.', + tmuxSession: 'test-session' + } + }; + + const messageResult = telegramChannel._generateTelegramMessage(approvalNotification, 'uuid-123', 'TEST1234'); + + if (messageResult.hasApproval) { + console.log(' ✅ Approval detected in notification'); + passed++; + } else { + console.log(' ❌ Approval not detected in notification'); + failed++; + } + + if (messageResult.approvalButtons && messageResult.approvalButtons.length > 0) { + console.log(' ✅ Approval buttons generated'); + passed++; + } else { + console.log(' ❌ Approval buttons not generated'); + failed++; + } + + if (messageResult.text.includes('⚠️')) { + console.log(' ✅ Warning indicator present'); + passed++; + } else { + console.log(' ❌ Warning indicator missing'); + failed++; + } + + console.log('\n' + '-'.repeat(60) + '\n'); + + // Test 5: Message Generation without Approval + console.log('📋 Test 5: Message Generation without Approval Request\n'); + + const regularNotification = { + type: 'completed', + project: 'test-project', + metadata: { + userQuestion: 'What is this codebase about?', + claudeResponse: 'This is a notification system for Claude Code.', + tmuxSession: 'test-session' + } + }; + + const regularResult = telegramChannel._generateTelegramMessage(regularNotification, 'uuid-456', 'TEST5678'); + + if (!regularResult.hasApproval) { + console.log(' ✅ No approval detected (correct)'); + passed++; + } else { + console.log(' ❌ Approval incorrectly detected'); + failed++; + } + + if (regularResult.approvalButtons === null) { + console.log(' ✅ No approval buttons (correct)'); + passed++; + } else { + console.log(' ❌ Approval buttons incorrectly generated'); + failed++; + } + + if (regularResult.text.includes('/cmd TEST5678')) { + console.log(' ✅ Command help included'); + passed++; + } else { + console.log(' ❌ Command help missing'); + failed++; + } + + console.log('\n' + '='.repeat(60) + '\n'); + + // Summary + const total = passed + failed; + console.log(`📊 Results: ${passed}/${total} tests passed\n`); + + if (failed === 0) { + console.log('✅ All tests passed!\n'); + } else { + console.log(`❌ ${failed} test(s) failed\n`); + process.exit(1); + } +} + +// Run tests +runTests().catch(console.error);