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 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
 
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "telegramToken": "your_telegram_bot_token_here",
3
+ "telegramWhitelist": [],
3
4
  "typingIntervalMs": 4000,
4
5
  "geminiCommand": "gemini",
5
6
  "geminiApprovalMode": "yolo",
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
- agentBridgeHome: 'AGENT_BRIDGE_HOME',
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
- agentBridgeHome: '~/.clawless',
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: 5000,
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
- '# Gemini Bridge Memory',
524
+ '# Clawless Memory',
496
525
  '',
497
- 'This file stores durable memory notes for Gemini Bridge.',
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: 5000,
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() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawless",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "A bridge connecting Telegram to Agent Gemini CLI using Agent Communication Protocol (ACP)",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",