clawless 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -1
- package/bin/cli.ts +5 -0
- package/clawless.config.example.json +1 -0
- package/dist/bin/cli.js +7 -2
- package/dist/index.js +46 -3
- package/dist/messaging/telegramClient.js +4 -0
- package/index.ts +52 -1
- package/messaging/telegramClient.ts +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -93,6 +93,30 @@ ACP_DEBUG_STREAM=false
|
|
|
93
93
|
4. Copy the token provided by BotFather
|
|
94
94
|
5. Paste it into your `.env` file
|
|
95
95
|
|
|
96
|
+
## Authorizing Users (Whitelist)
|
|
97
|
+
|
|
98
|
+
For security, the bot only accepts commands from authorized users. To configure:
|
|
99
|
+
|
|
100
|
+
1. **Use your Telegram username**:
|
|
101
|
+
- You can use your Telegram username (e.g., `your_username` or `@your_username`).
|
|
102
|
+
- If you don't have a username set, you must create one in Telegram settings.
|
|
103
|
+
|
|
104
|
+
2. **Add usernames to whitelist** in `~/.clawless/config.json`:
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"telegramToken": "your_bot_token",
|
|
108
|
+
"telegramWhitelist": ["your_username", "another_user"]
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
3. **Alternative: Use environment variable**:
|
|
113
|
+
```bash
|
|
114
|
+
# Must be a valid JSON array string
|
|
115
|
+
TELEGRAM_WHITELIST='["your_username", "another_user"]'
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
⚠️ **Security Note**: If `telegramWhitelist` is empty or not configured, **all users will be blocked** by default. This is a safety measure to prevent unauthorized access.
|
|
119
|
+
|
|
96
120
|
## Usage
|
|
97
121
|
|
|
98
122
|
### CLI Mode
|
|
@@ -210,6 +234,7 @@ pm2 save
|
|
|
210
234
|
| Variable | Required | Default | Description |
|
|
211
235
|
|----------|----------|---------|-------------|
|
|
212
236
|
| `TELEGRAM_TOKEN` | Yes | - | Your Telegram bot token from BotFather |
|
|
237
|
+
| `TELEGRAM_WHITELIST` | No | [] | List of authorized Telegram usernames. **Security:** If empty, all users are blocked by default. Format: JSON array `["username1", "username2"]` |
|
|
213
238
|
| `TYPING_INTERVAL_MS` | No | 4000 | Interval (in milliseconds) for refreshing Telegram typing status |
|
|
214
239
|
| `GEMINI_TIMEOUT_MS` | No | 900000 | Overall timeout for a single Gemini CLI run |
|
|
215
240
|
| `GEMINI_NO_OUTPUT_TIMEOUT_MS` | No | 60000 | Idle timeout; aborts if Gemini emits no output for this duration |
|
|
@@ -414,7 +439,7 @@ The codebase is designed to be simple and extensible:
|
|
|
414
439
|
- **Never commit** `.env` file with your token (it's in `.gitignore`)
|
|
415
440
|
- **Rotate tokens** if accidentally exposed
|
|
416
441
|
- **Limit bot access** using Telegram's bot settings
|
|
417
|
-
- **Monitor logs** for unusual activity
|
|
442
|
+
- **Monitor logs** for unusual activity and unauthorized access attempts
|
|
418
443
|
|
|
419
444
|
## Contributing
|
|
420
445
|
|
package/bin/cli.ts
CHANGED
|
@@ -7,6 +7,7 @@ import process from 'node:process';
|
|
|
7
7
|
|
|
8
8
|
const ENV_KEY_MAP: Record<string, string> = {
|
|
9
9
|
telegramToken: 'TELEGRAM_TOKEN',
|
|
10
|
+
telegramWhitelist: 'TELEGRAM_WHITELIST',
|
|
10
11
|
typingIntervalMs: 'TYPING_INTERVAL_MS',
|
|
11
12
|
geminiCommand: 'GEMINI_COMMAND',
|
|
12
13
|
geminiApprovalMode: 'GEMINI_APPROVAL_MODE',
|
|
@@ -34,6 +35,7 @@ const DEFAULT_AGENT_BRIDGE_HOME = path.join(os.homedir(), '.clawless');
|
|
|
34
35
|
const DEFAULT_MEMORY_FILE_PATH = path.join(DEFAULT_AGENT_BRIDGE_HOME, 'MEMORY.md');
|
|
35
36
|
const DEFAULT_CONFIG_TEMPLATE = {
|
|
36
37
|
telegramToken: 'your_telegram_bot_token_here',
|
|
38
|
+
telegramWhitelist: [],
|
|
37
39
|
typingIntervalMs: 4000,
|
|
38
40
|
geminiCommand: 'gemini',
|
|
39
41
|
geminiApprovalMode: 'yolo',
|
|
@@ -109,6 +111,9 @@ function toEnvValue(value: unknown) {
|
|
|
109
111
|
if (typeof value === 'string') {
|
|
110
112
|
return value;
|
|
111
113
|
}
|
|
114
|
+
if (typeof value === 'object') {
|
|
115
|
+
return JSON.stringify(value);
|
|
116
|
+
}
|
|
112
117
|
return String(value);
|
|
113
118
|
}
|
|
114
119
|
|
package/dist/bin/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import process from 'node:process';
|
|
6
6
|
const ENV_KEY_MAP = {
|
|
7
7
|
telegramToken: 'TELEGRAM_TOKEN',
|
|
8
|
+
telegramWhitelist: 'TELEGRAM_WHITELIST',
|
|
8
9
|
typingIntervalMs: 'TYPING_INTERVAL_MS',
|
|
9
10
|
geminiCommand: 'GEMINI_COMMAND',
|
|
10
11
|
geminiApprovalMode: 'GEMINI_APPROVAL_MODE',
|
|
@@ -21,7 +22,7 @@ const ENV_KEY_MAP = {
|
|
|
21
22
|
callbackPort: 'CALLBACK_PORT',
|
|
22
23
|
callbackAuthToken: 'CALLBACK_AUTH_TOKEN',
|
|
23
24
|
callbackMaxBodyBytes: 'CALLBACK_MAX_BODY_BYTES',
|
|
24
|
-
|
|
25
|
+
ClawlessHome: 'AGENT_BRIDGE_HOME',
|
|
25
26
|
memoryFilePath: 'MEMORY_FILE_PATH',
|
|
26
27
|
memoryMaxChars: 'MEMORY_MAX_CHARS',
|
|
27
28
|
schedulesFilePath: 'SCHEDULES_FILE_PATH',
|
|
@@ -31,6 +32,7 @@ const DEFAULT_AGENT_BRIDGE_HOME = path.join(os.homedir(), '.clawless');
|
|
|
31
32
|
const DEFAULT_MEMORY_FILE_PATH = path.join(DEFAULT_AGENT_BRIDGE_HOME, 'MEMORY.md');
|
|
32
33
|
const DEFAULT_CONFIG_TEMPLATE = {
|
|
33
34
|
telegramToken: 'your_telegram_bot_token_here',
|
|
35
|
+
telegramWhitelist: [],
|
|
34
36
|
typingIntervalMs: 4000,
|
|
35
37
|
geminiCommand: 'gemini',
|
|
36
38
|
geminiApprovalMode: 'yolo',
|
|
@@ -47,7 +49,7 @@ const DEFAULT_CONFIG_TEMPLATE = {
|
|
|
47
49
|
callbackPort: 8788,
|
|
48
50
|
callbackAuthToken: '',
|
|
49
51
|
callbackMaxBodyBytes: 65536,
|
|
50
|
-
|
|
52
|
+
ClawlessHome: '~/.clawless',
|
|
51
53
|
memoryFilePath: '~/.clawless/MEMORY.md',
|
|
52
54
|
memoryMaxChars: 12000,
|
|
53
55
|
schedulesFilePath: '~/.clawless/schedules.json',
|
|
@@ -98,6 +100,9 @@ function toEnvValue(value) {
|
|
|
98
100
|
if (typeof value === 'string') {
|
|
99
101
|
return value;
|
|
100
102
|
}
|
|
103
|
+
if (typeof value === 'object') {
|
|
104
|
+
return JSON.stringify(value);
|
|
105
|
+
}
|
|
101
106
|
return String(value);
|
|
102
107
|
}
|
|
103
108
|
function resolveEnvKey(configKey) {
|
package/dist/index.js
CHANGED
|
@@ -48,6 +48,35 @@ const TYPING_INTERVAL_MS = parseInt(process.env.TYPING_INTERVAL_MS || '4000', 10
|
|
|
48
48
|
const TELEGRAM_STREAM_UPDATE_INTERVAL_MS = 1000;
|
|
49
49
|
// Maximum response length to prevent memory issues (Telegram has 4096 char limit anyway)
|
|
50
50
|
const MAX_RESPONSE_LENGTH = parseInt(process.env.MAX_RESPONSE_LENGTH || '4000', 10);
|
|
51
|
+
// Parse Telegram whitelist from environment variable
|
|
52
|
+
// Expected format: JSON array of usernames (e.g., ["user1", "user2"])
|
|
53
|
+
function parseWhitelistFromEnv(envValue) {
|
|
54
|
+
if (!envValue || envValue.trim() === '') {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(envValue);
|
|
59
|
+
if (Array.isArray(parsed)) {
|
|
60
|
+
return parsed.map((name) => String(name).trim().replace(/^@/, '')).filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
console.warn('Warning: TELEGRAM_WHITELIST must be a valid JSON array of usernames (e.g., ["user1", "user2"])');
|
|
65
|
+
}
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
const TELEGRAM_WHITELIST = parseWhitelistFromEnv(process.env.TELEGRAM_WHITELIST || '');
|
|
69
|
+
function isUserAuthorized(username) {
|
|
70
|
+
// If whitelist is empty, block all users by default (safe default)
|
|
71
|
+
if (TELEGRAM_WHITELIST.length === 0) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
if (!username) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const normalizedUsername = username.toLowerCase();
|
|
78
|
+
return TELEGRAM_WHITELIST.some(entry => entry.toLowerCase() === normalizedUsername);
|
|
79
|
+
}
|
|
51
80
|
const messagingClient = new TelegramMessagingClient({
|
|
52
81
|
token: process.env.TELEGRAM_TOKEN,
|
|
53
82
|
typingIntervalMs: TYPING_INTERVAL_MS,
|
|
@@ -68,7 +97,7 @@ const GEMINI_STDERR_TAIL_MAX = 4000;
|
|
|
68
97
|
function validateGeminiCommandOrExit() {
|
|
69
98
|
const result = spawnSync(GEMINI_COMMAND, ['--version'], {
|
|
70
99
|
stdio: 'ignore',
|
|
71
|
-
timeout:
|
|
100
|
+
timeout: 10000,
|
|
72
101
|
killSignal: 'SIGKILL',
|
|
73
102
|
});
|
|
74
103
|
if (result.error?.code === 'ENOENT') {
|
|
@@ -492,9 +521,9 @@ function ensureMemoryFile() {
|
|
|
492
521
|
fs.mkdirSync(path.dirname(MEMORY_FILE_PATH), { recursive: true });
|
|
493
522
|
if (!fs.existsSync(MEMORY_FILE_PATH)) {
|
|
494
523
|
const template = [
|
|
495
|
-
'#
|
|
524
|
+
'# Clawless Memory',
|
|
496
525
|
'',
|
|
497
|
-
'This file stores durable memory notes for
|
|
526
|
+
'This file stores durable memory notes for Clawless.',
|
|
498
527
|
'',
|
|
499
528
|
'## Notes',
|
|
500
529
|
'',
|
|
@@ -1034,6 +1063,12 @@ async function processSingleMessage(messageContext, messageRequestId) {
|
|
|
1034
1063
|
* Handles incoming text messages from Telegram
|
|
1035
1064
|
*/
|
|
1036
1065
|
messagingClient.onTextMessage(async (messageContext) => {
|
|
1066
|
+
// Check if user is authorized
|
|
1067
|
+
if (!isUserAuthorized(messageContext.username)) {
|
|
1068
|
+
console.warn(`Unauthorized access attempt from username: ${messageContext.username ?? 'none'} (ID: ${messageContext.userId ?? 'unknown'})`);
|
|
1069
|
+
await messageContext.sendText('🚫 Unauthorized. This bot is restricted to authorized users only.');
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1037
1072
|
if (messageContext.chatId !== undefined && messageContext.chatId !== null) {
|
|
1038
1073
|
lastIncomingChatId = String(messageContext.chatId);
|
|
1039
1074
|
persistCallbackChatId(lastIncomingChatId);
|
|
@@ -1092,7 +1127,15 @@ messagingClient.launch()
|
|
|
1092
1127
|
callbackPort: CALLBACK_PORT,
|
|
1093
1128
|
mcpSkillsSource: 'local Gemini CLI defaults (no MCP override)',
|
|
1094
1129
|
acpMode: `${GEMINI_COMMAND} --experimental-acp`,
|
|
1130
|
+
telegramWhitelist: TELEGRAM_WHITELIST.length > 0 ? `${TELEGRAM_WHITELIST.length} user(s) authorized` : 'NONE (all users blocked)',
|
|
1095
1131
|
});
|
|
1132
|
+
if (TELEGRAM_WHITELIST.length === 0) {
|
|
1133
|
+
console.warn('⚠️ WARNING: Telegram whitelist is empty. All users will be blocked.');
|
|
1134
|
+
console.warn('⚠️ Add usernames to TELEGRAM_WHITELIST config (as a JSON array) to authorize users.');
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
console.log(`✅ Telegram authorization enabled. Authorized usernames: ${TELEGRAM_WHITELIST.join(', ')}`);
|
|
1138
|
+
}
|
|
1096
1139
|
scheduleAcpPrewarm('post-launch');
|
|
1097
1140
|
if (HEARTBEAT_INTERVAL_MS > 0) {
|
|
1098
1141
|
setInterval(() => {
|
|
@@ -22,12 +22,16 @@ class TelegramMessageContext {
|
|
|
22
22
|
maxMessageLength;
|
|
23
23
|
text;
|
|
24
24
|
chatId;
|
|
25
|
+
userId;
|
|
26
|
+
username;
|
|
25
27
|
constructor(ctx, typingIntervalMs, maxMessageLength) {
|
|
26
28
|
this.ctx = ctx;
|
|
27
29
|
this.typingIntervalMs = typingIntervalMs;
|
|
28
30
|
this.maxMessageLength = maxMessageLength;
|
|
29
31
|
this.text = ctx.message?.text || '';
|
|
30
32
|
this.chatId = ctx.chat?.id;
|
|
33
|
+
this.userId = ctx.from?.id;
|
|
34
|
+
this.username = ctx.from?.username;
|
|
31
35
|
}
|
|
32
36
|
startTyping() {
|
|
33
37
|
this.ctx.telegram.sendChatAction(this.ctx.chat.id, 'typing').catch(() => { });
|
package/index.ts
CHANGED
|
@@ -55,6 +55,42 @@ const TELEGRAM_STREAM_UPDATE_INTERVAL_MS = 1000;
|
|
|
55
55
|
// Maximum response length to prevent memory issues (Telegram has 4096 char limit anyway)
|
|
56
56
|
const MAX_RESPONSE_LENGTH = parseInt(process.env.MAX_RESPONSE_LENGTH || '4000', 10);
|
|
57
57
|
|
|
58
|
+
// Parse Telegram whitelist from environment variable
|
|
59
|
+
// Expected format: JSON array of usernames (e.g., ["user1", "user2"])
|
|
60
|
+
function parseWhitelistFromEnv(envValue: string): string[] {
|
|
61
|
+
if (!envValue || envValue.trim() === '') {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(envValue);
|
|
67
|
+
if (Array.isArray(parsed)) {
|
|
68
|
+
return parsed.map((name) => String(name).trim().replace(/^@/, '')).filter(Boolean);
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
console.warn('Warning: TELEGRAM_WHITELIST must be a valid JSON array of usernames (e.g., ["user1", "user2"])');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const TELEGRAM_WHITELIST: string[] = parseWhitelistFromEnv(process.env.TELEGRAM_WHITELIST || '');
|
|
78
|
+
|
|
79
|
+
function isUserAuthorized(username: string | undefined): boolean {
|
|
80
|
+
// If whitelist is empty, block all users by default (safe default)
|
|
81
|
+
if (TELEGRAM_WHITELIST.length === 0) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!username) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const normalizedUsername = username.toLowerCase();
|
|
90
|
+
|
|
91
|
+
return TELEGRAM_WHITELIST.some(entry => entry.toLowerCase() === normalizedUsername);
|
|
92
|
+
}
|
|
93
|
+
|
|
58
94
|
const messagingClient = new TelegramMessagingClient({
|
|
59
95
|
token: process.env.TELEGRAM_TOKEN,
|
|
60
96
|
typingIntervalMs: TYPING_INTERVAL_MS,
|
|
@@ -77,7 +113,7 @@ const GEMINI_STDERR_TAIL_MAX = 4000;
|
|
|
77
113
|
function validateGeminiCommandOrExit() {
|
|
78
114
|
const result = spawnSync(GEMINI_COMMAND, ['--version'], {
|
|
79
115
|
stdio: 'ignore',
|
|
80
|
-
timeout:
|
|
116
|
+
timeout: 10000,
|
|
81
117
|
killSignal: 'SIGKILL',
|
|
82
118
|
});
|
|
83
119
|
|
|
@@ -1182,6 +1218,13 @@ async function processSingleMessage(messageContext: any, messageRequestId: numbe
|
|
|
1182
1218
|
* Handles incoming text messages from Telegram
|
|
1183
1219
|
*/
|
|
1184
1220
|
messagingClient.onTextMessage(async (messageContext) => {
|
|
1221
|
+
// Check if user is authorized
|
|
1222
|
+
if (!isUserAuthorized(messageContext.username)) {
|
|
1223
|
+
console.warn(`Unauthorized access attempt from username: ${messageContext.username ?? 'none'} (ID: ${messageContext.userId ?? 'unknown'})`);
|
|
1224
|
+
await messageContext.sendText('🚫 Unauthorized. This bot is restricted to authorized users only.');
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1185
1228
|
if (messageContext.chatId !== undefined && messageContext.chatId !== null) {
|
|
1186
1229
|
lastIncomingChatId = String(messageContext.chatId);
|
|
1187
1230
|
persistCallbackChatId(lastIncomingChatId);
|
|
@@ -1246,8 +1289,16 @@ messagingClient.launch()
|
|
|
1246
1289
|
callbackPort: CALLBACK_PORT,
|
|
1247
1290
|
mcpSkillsSource: 'local Gemini CLI defaults (no MCP override)',
|
|
1248
1291
|
acpMode: `${GEMINI_COMMAND} --experimental-acp`,
|
|
1292
|
+
telegramWhitelist: TELEGRAM_WHITELIST.length > 0 ? `${TELEGRAM_WHITELIST.length} user(s) authorized` : 'NONE (all users blocked)',
|
|
1249
1293
|
});
|
|
1250
1294
|
|
|
1295
|
+
if (TELEGRAM_WHITELIST.length === 0) {
|
|
1296
|
+
console.warn('⚠️ WARNING: Telegram whitelist is empty. All users will be blocked.');
|
|
1297
|
+
console.warn('⚠️ Add usernames to TELEGRAM_WHITELIST config (as a JSON array) to authorize users.');
|
|
1298
|
+
} else {
|
|
1299
|
+
console.log(`✅ Telegram authorization enabled. Authorized usernames: ${TELEGRAM_WHITELIST.join(', ')}`);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1251
1302
|
scheduleAcpPrewarm('post-launch');
|
|
1252
1303
|
|
|
1253
1304
|
if (HEARTBEAT_INTERVAL_MS > 0) {
|
|
@@ -28,6 +28,8 @@ class TelegramMessageContext {
|
|
|
28
28
|
maxMessageLength: number;
|
|
29
29
|
text: string;
|
|
30
30
|
chatId: string | number | undefined;
|
|
31
|
+
userId: number | undefined;
|
|
32
|
+
username: string | undefined;
|
|
31
33
|
|
|
32
34
|
constructor(ctx: any, typingIntervalMs: number, maxMessageLength: number) {
|
|
33
35
|
this.ctx = ctx;
|
|
@@ -35,6 +37,8 @@ class TelegramMessageContext {
|
|
|
35
37
|
this.maxMessageLength = maxMessageLength;
|
|
36
38
|
this.text = ctx.message?.text || '';
|
|
37
39
|
this.chatId = ctx.chat?.id;
|
|
40
|
+
this.userId = ctx.from?.id;
|
|
41
|
+
this.username = ctx.from?.username;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
startTyping() {
|