clawless 0.2.0 → 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
@@ -1,12 +1,42 @@
1
- # Telegram-Gemini ACP Bridge
1
+ # Clawless — Bring Your Own Agent (Interface + ACP)
2
2
 
3
- A bridge that connects a Telegram bot to the Gemini CLI using the Agent Communication Protocol (ACP). This turns Telegram into a "remote terminal" for your local Gemini agent, enabling rich tool use and persistent context.
3
+ Clawless is an interface bridge built around one core idea: **Bring Your Own Agent**.
4
+
5
+ Instead of forcing a built-in runtime, Clawless lets you keep your preferred local ACP-capable CLI (Gemini CLI by default) and adds a reliable interface layer, callbacks, and scheduling on top.
6
+
7
+ Today, Telegram is the first interface adapter; more interfaces are planned.
8
+
9
+ ## Bring Your Own Agent (Main Value)
10
+
11
+ Clawless is designed so your messaging layer and automation layer stay stable while your agent runtime can change.
12
+
13
+ - Keep your preferred local agent CLI workflow
14
+ - Keep your existing MCP tools and local files
15
+ - Swap runtimes without rebuilding your bot integration
16
+ - Avoid lock-in to a single all-in-one framework
17
+
18
+ ## Why Clawless
19
+
20
+ If you have tried heavier all-in-one agent frameworks, Clawless is the minimal alternative:
21
+
22
+ - **BYO-agent first**: use your preferred local ACP-capable CLI runtime
23
+ - **Lightweight setup**: minimal glue instead of a full platform migration
24
+ - **Local-first control**: your machine, your tools, your data flow
25
+ - **Transport only**: interface layer is separate from the agent runtime
26
+
27
+ ## Interface Adapters
28
+
29
+ - **Current adapter**: Telegram
30
+ - **Planned direction**: add more interfaces without changing core agent orchestration
31
+ - **Design goal**: keep one message context contract so new interfaces reuse queueing, callbacks, scheduler, and ACP flow
4
32
 
5
33
  ## Features
6
34
 
7
- - 🤖 **Telegram Bot Interface**: Interact with Gemini CLI through Telegram
8
- - ⌨️ **Typing Status UX**: Shows Telegram typing indicator while Gemini is processing
9
- - 🛠️ **Rich Tool Support**: Leverages MCP (Model Context Protocol) servers connected to Gemini CLI
35
+ - 🔀 **Bring Your Own Agent Runtime**: Keep Telegram/callback/scheduler UX while choosing your preferred local ACP-capable CLI
36
+ - 🔌 **Adapter-Friendly Interface Layer**: Telegram today, additional interfaces planned
37
+ - 🤖 **Telegram (Current Adapter)**: Interact with your local agent runtime through Telegram
38
+ - ⌨️ **Typing Status UX**: Shows Telegram typing indicator while the agent is processing
39
+ - 🛠️ **Rich Tool Support**: Leverages MCP (Model Context Protocol) servers connected to your local CLI runtime
10
40
  - 🔒 **Privacy**: Runs on your hardware, you control data flow
11
41
  - 💾 **Persistent Context**: Maintains local session unlike standard API calls
12
42
  - 📬 **Sequential Queueing**: Processes one message at a time to avoid overlap and races
@@ -16,22 +46,22 @@ A bridge that connects a Telegram bot to the Gemini CLI using the Agent Communic
16
46
  ## Architecture
17
47
 
18
48
  ```
19
- ┌──────────┐ ┌───────────┐ ┌─────────────┐
20
- Telegram │ ◄─────► │ Bridge ◄─────► Gemini CLI │
21
- User (Node.js) ACP (Local)
22
- └──────────┘ └───────────┘ └─────────────┘
49
+ ┌──────────────────────┐ ┌────────────────┐ ┌──────────────────────────┐
50
+ Interface Adapter │◄───►│ Clawless │◄───►│ Local Agent.
51
+ (Telegram now) (Node.js) ACP e.g. Gemini CLI (default)│
52
+ └──────────────────────┘ └────────────────┘ └──────────────────────────┘
23
53
  ```
24
54
 
25
55
  The bridge:
26
- 1. Receives messages from Telegram users
27
- 2. Forwards them to the Gemini CLI via ACP (Agent Communication Protocol)
28
- 3. Shows typing status, then sends a single final response
56
+ 1. Receives messages from the active interface adapter (Telegram today)
57
+ 2. Forwards them to **your configured local agent CLI** via ACP (Agent Communication Protocol)
58
+ 3. Sends interface-appropriate progress/status updates, then returns a single final response
29
59
 
30
60
  ## Prerequisites
31
61
 
32
62
  - **Node.js** 18.0.0 or higher
33
- - **Gemini CLI** installed and configured with ACP support
34
- - **Telegram Bot Token** from [@BotFather](https://t.me/BotFather)
63
+ - **A local ACP-capable agent CLI** installed and configured (Gemini CLI is the default setup)
64
+ - **Telegram Bot Token** from [@BotFather](https://t.me/BotFather) for the current Telegram adapter
35
65
 
36
66
  ## Installation
37
67
 
@@ -63,6 +93,30 @@ ACP_DEBUG_STREAM=false
63
93
  4. Copy the token provided by BotFather
64
94
  5. Paste it into your `.env` file
65
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
+
66
120
  ## Usage
67
121
 
68
122
  ### CLI Mode
@@ -73,6 +127,8 @@ After install, the package exposes a CLI command:
73
127
  clawless
74
128
  ```
75
129
 
130
+ > Note: the binary name is currently `clawless` for compatibility, while the project name is Clawless.
131
+
76
132
  Local development alternatives:
77
133
 
78
134
  ```bash
@@ -178,6 +234,7 @@ pm2 save
178
234
  | Variable | Required | Default | Description |
179
235
  |----------|----------|---------|-------------|
180
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"]` |
181
238
  | `TYPING_INTERVAL_MS` | No | 4000 | Interval (in milliseconds) for refreshing Telegram typing status |
182
239
  | `GEMINI_TIMEOUT_MS` | No | 900000 | Overall timeout for a single Gemini CLI run |
183
240
  | `GEMINI_NO_OUTPUT_TIMEOUT_MS` | No | 60000 | Idle timeout; aborts if Gemini emits no output for this duration |
@@ -193,7 +250,7 @@ pm2 save
193
250
  | `CALLBACK_PORT` | No | 8788 | Bind port for callback server |
194
251
  | `CALLBACK_AUTH_TOKEN` | No | - | Optional bearer/token guard for callback endpoint |
195
252
  | `CALLBACK_MAX_BODY_BYTES` | No | 65536 | Maximum accepted callback request body size |
196
- | `AGENT_BRIDGE_HOME` | No | ~/.clawless | Home directory for Gemini Bridge runtime files |
253
+ | `AGENT_BRIDGE_HOME` | No | ~/.clawless | Home directory for Clawless runtime files |
197
254
  | `MEMORY_FILE_PATH` | No | ~/.clawless/MEMORY.md | Persistent memory file path injected into Gemini prompt context |
198
255
  | `MEMORY_MAX_CHARS` | No | 12000 | Max memory-file characters injected into prompt context |
199
256
  | `SCHEDULES_FILE_PATH` | No | ~/.clawless/schedules.json | Persistent scheduler storage file |
@@ -229,7 +286,7 @@ curl -sS -X POST "http://127.0.0.1:8788/callback/telegram" \
229
286
 
230
287
  ### Scheduler API
231
288
 
232
- The bridge includes a built-in cron scheduler that allows you to schedule tasks to be executed through Gemini CLI:
289
+ The bridge includes a built-in cron scheduler that allows you to schedule tasks to be executed through your configured local agent CLI:
233
290
 
234
291
  - Schedules are persisted to disk and automatically reloaded on restart.
235
292
  - Default storage path: `~/.clawless/schedules.json` (override with `SCHEDULES_FILE_PATH`).
@@ -256,7 +313,7 @@ curl -X POST http://127.0.0.1:8788/api/schedule \
256
313
  }'
257
314
  ```
258
315
 
259
- When a scheduled job runs, it executes the message through Gemini CLI and sends the response to your Telegram chat.
316
+ When a scheduled job runs, it executes the message through your configured local agent runtime and sends the response to your Telegram chat.
260
317
 
261
318
  **Ask Gemini to create schedules naturally:**
262
319
  - "Remind me to take a break in 30 minutes"
@@ -268,10 +325,10 @@ See [SCHEDULER.md](SCHEDULER.md) for complete API documentation.
268
325
  ### Persistent Memory File
269
326
 
270
327
  - The bridge ensures a memory file exists at `~/.clawless/MEMORY.md` on startup.
271
- - Gemini is started with include access to both `~/.clawless` and your full home directory (`~/`).
328
+ - The configured local agent CLI is started with include access to both `~/.clawless` and your full home directory (`~/`).
272
329
  - ACP session setup uses the required `mcpServers` field with an empty array and relies on Gemini CLI runtime defaults for MCP/skills loading.
273
330
  - Each prompt includes memory instructions and current `MEMORY.md` content.
274
- - When asked to memorize/remember something, Gemini is instructed to append new notes under `## Notes`.
331
+ - When asked to memorize/remember something, the agent is instructed to append new notes under `## Notes`.
275
332
 
276
333
  ### Timeout Tuning
277
334
 
@@ -296,28 +353,31 @@ The `MAX_RESPONSE_LENGTH` prevents memory issues with very long responses:
296
353
  1. **User sends a message** via Telegram
297
354
  2. **Bridge queues** the message if another request is in progress
298
355
  3. **Worker dequeues** the next message when prior processing completes
299
- 4. **Gemini run starts** and typing status is shown in Telegram
300
- 5. **Single final reply** is sent when Gemini finishes
356
+ 4. **Agent run starts** and typing status is shown in Telegram
357
+ 5. **Single final reply** is sent when the run finishes
301
358
 
302
359
  ### Queueing Behavior
303
360
 
304
361
  The bridge uses a single-worker in-memory queue:
305
- - Prevents overlapping Gemini runs
362
+ - Prevents overlapping agent runs
306
363
  - Preserves message order
307
364
  - Avoids duplicate-edit/fallback races from message updates
308
365
 
309
366
  ## Advantages Over Standard API Bots
310
367
 
311
- 1. **Persistent Context**: The Gemini CLI maintains a local session, unlike stateless API calls
312
- 2. **Local File Access**: Can access files on your server if configured
313
- 3. **MCP Tool Integration**: Automatically uses tools from connected MCP servers (Calendar, Database, etc.)
314
- 4. **Privacy Control**: Runs on your hardware, you control data processing
315
- 5. **Custom Configuration**: Use your specific Gemini CLI setup and preferences
368
+ 1. **BYO-Agent Flexibility**: Keep the same bridge while choosing or changing your local CLI runtime
369
+ 2. **Persistent Context**: The local agent CLI maintains a local session, unlike stateless API calls
370
+ 3. **Local File Access**: Can access files on your server if configured
371
+ 4. **MCP Tool Integration**: Uses tools from connected MCP servers (Calendar, Database, etc.)
372
+ 5. **Privacy Control**: Runs on your hardware, you control data processing
373
+ 6. **Custom Configuration**: Use your specific local CLI setup and preferences
316
374
 
317
375
  ## Troubleshooting
318
376
 
319
377
  ### Bot doesn't respond
320
378
 
379
+ For the default Gemini CLI setup:
380
+
321
381
  1. Check if Gemini CLI is installed:
322
382
  ```bash
323
383
  which gemini
@@ -347,22 +407,29 @@ If you see "429 Too Many Requests" errors:
347
407
  ### Project Structure
348
408
 
349
409
  ```
350
- RemoteAgent/
351
- ├── index.ts # Main bridge application
410
+ Clawless/
411
+ ├── index.ts # Main bridge application
412
+ ├── bin/
413
+ │ └── cli.ts # CLI entrypoint
352
414
  ├── messaging/
353
- │ └── telegramClient.ts # Telegram adapter implementing neutral message context
354
- ├── package.json # Node.js dependencies
355
- ├── ecosystem.config.json # PM2 configuration
356
- ├── .env.example # Environment variables template
357
- ├── .env # Your local configuration (not in git)
358
- └── README.md # This file
415
+ │ └── telegramClient.ts # Telegram adapter
416
+ ├── scheduler/
417
+ ├── cronScheduler.ts # Schedule persistence + cron orchestration
418
+ │ └── scheduledJobHandler.ts # Scheduled run execution logic
419
+ ├── acp/
420
+ │ ├── tempAcpRunner.ts # Isolated ACP run helper
421
+ │ └── clientHelpers.ts # ACP helper utilities
422
+ ├── package.json # Node.js dependencies
423
+ ├── ecosystem.config.json # PM2 configuration
424
+ ├── clawless.config.example.json # CLI config template
425
+ └── README.md # This file
359
426
  ```
360
427
 
361
428
  ### Adding Features
362
429
 
363
430
  The codebase is designed to be simple and extensible:
364
431
  - Core queue + ACP logic is in `index.ts`
365
- - Messaging-platform specifics live in `messaging/telegramClient.ts`
432
+ - Interface-specific messaging logic lives in `messaging/telegramClient.ts`
366
433
  - New bot platforms can implement the same message context shape (`text`, `startTyping()`, `sendText()`)
367
434
  - Error handling is centralized
368
435
  - Rate limiting logic is configurable
@@ -372,7 +439,7 @@ The codebase is designed to be simple and extensible:
372
439
  - **Never commit** `.env` file with your token (it's in `.gitignore`)
373
440
  - **Rotate tokens** if accidentally exposed
374
441
  - **Limit bot access** using Telegram's bot settings
375
- - **Monitor logs** for unusual activity
442
+ - **Monitor logs** for unusual activity and unauthorized access attempts
376
443
 
377
444
  ## Contributing
378
445
 
@@ -401,4 +468,4 @@ For issues and questions:
401
468
 
402
469
  ---
403
470
 
404
- **Note**: This bridge requires a working Gemini CLI installation with ACP support. Ensure your CLI is properly configured before running the bridge.
471
+ **Note**: This bridge requires a working local ACP-capable CLI (Gemini CLI is the default setup). Ensure your CLI is properly configured before running the bridge.
package/SCHEDULER.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # Scheduler API Documentation
2
2
 
3
- The Clawless now includes a cron scheduler API that allows you to schedule tasks to be executed through Gemini CLI at specific times or on a recurring basis.
3
+ Clawless includes a cron scheduler API that allows you to schedule tasks to be executed through your local agent CLI at specific times or on a recurring basis (Gemini CLI by default).
4
4
 
5
5
  ## Overview
6
6
 
7
7
  When a scheduled job runs:
8
- 1. The scheduled message is sent to a standalone Gemini CLI session
9
- 2. Gemini processes the message and generates a response
10
- 3. The response is sent back to your Telegram bot
11
- 4. The result appears in your Telegram chat
8
+ 1. The scheduled message is sent to a standalone local agent CLI session
9
+ 2. The agent processes the message and generates a response
10
+ 3. The response is sent back through the active interface adapter
11
+ 4. With the current Telegram adapter, the result appears in your Telegram chat
12
12
 
13
13
  Schedules are persisted to disk and reloaded on startup. By default the file is `~/.clawless/schedules.json` and can be overridden via `SCHEDULES_FILE_PATH`.
14
14
 
@@ -43,7 +43,7 @@ curl -X POST http://127.0.0.1:8788/api/schedule \
43
43
  **Request Body:**
44
44
  ```json
45
45
  {
46
- "message": "The prompt to send to Gemini CLI",
46
+ "message": "The prompt to send to your local agent CLI",
47
47
  "description": "Optional description of the schedule",
48
48
  "cronExpression": "0 9 * * *"
49
49
  }
@@ -83,7 +83,7 @@ curl -X POST http://127.0.0.1:8788/api/schedule \
83
83
  **Request Body:**
84
84
  ```json
85
85
  {
86
- "message": "The prompt to send to Gemini CLI",
86
+ "message": "The prompt to send to your local agent CLI",
87
87
  "description": "Optional description",
88
88
  "oneTime": true,
89
89
  "runAt": "2026-02-13T15:30:00Z"
@@ -180,39 +180,39 @@ curl -X DELETE http://127.0.0.1:8788/api/schedule/schedule_1707835800000_abc123
180
180
  }
181
181
  ```
182
182
 
183
- ## Using with Gemini CLI
183
+ ## Using with Your Local CLI (Default: Gemini)
184
184
 
185
- The Gemini CLI is aware of the scheduler API through the system prompt. You can ask Gemini to create schedules naturally:
185
+ The configured local CLI is aware of the scheduler API through the system prompt. With the default setup, you can ask Gemini to create schedules naturally:
186
186
 
187
187
  **Examples:**
188
188
 
189
189
  1. **"Remind me to take a break in 30 minutes"**
190
- - Gemini will create a one-time schedule
190
+ - The agent will create a one-time schedule
191
191
 
192
192
  2. **"Check my calendar every morning at 9am and send me a summary"**
193
- - Gemini will create a recurring schedule with cron expression `0 9 * * *`
193
+ - The agent will create a recurring schedule with cron expression `0 9 * * *`
194
194
 
195
195
  3. **"Every Friday at 5pm, remind me to review my weekly goals"**
196
- - Gemini will create a recurring schedule with cron expression `0 17 * * 5`
196
+ - The agent will create a recurring schedule with cron expression `0 17 * * 5`
197
197
 
198
198
  4. **"List my scheduled tasks"**
199
- - Gemini will query the schedule API and show you all active schedules
199
+ - The agent will query the schedule API and show you all active schedules
200
200
 
201
201
  5. **"Cancel the calendar summary schedule"**
202
- - Gemini will find and delete the matching schedule
202
+ - The agent will find and delete the matching schedule
203
203
 
204
204
  ## How It Works
205
205
 
206
- 1. When you ask Gemini to create a schedule, it will:
206
+ 1. When you ask the agent to create a schedule, it will:
207
207
  - Parse your request to determine timing (cron expression or specific date/time)
208
208
  - Call the scheduler API with appropriate parameters
209
209
  - Confirm the schedule was created
210
210
 
211
211
  2. When the scheduled time arrives:
212
212
  - The scheduler executes the job
213
- - The message is sent to a new Gemini CLI session
214
- - Gemini processes the message (can use tools, access files, etc.)
215
- - The response is sent to your Telegram chat
213
+ - The message is sent to a new local CLI session
214
+ - The agent processes the message (can use tools, access files, etc.)
215
+ - The response is sent to the active interface destination (Telegram with current adapter)
216
216
 
217
217
  3. For recurring schedules:
218
218
  - The job runs according to the cron expression
@@ -226,9 +226,9 @@ The Gemini CLI is aware of the scheduler API through the system prompt. You can
226
226
  ## Notes
227
227
 
228
228
  - Schedules are stored in memory and will be lost if the bridge restarts
229
- - Make sure your Telegram bot has received at least one message so it knows where to send results
229
+ - For the current Telegram adapter, make sure your bot has received at least one message so it knows where to send results
230
230
  - The timezone used for cron schedules is determined by the `TZ` environment variable (defaults to UTC)
231
- - Scheduled jobs run in separate Gemini CLI sessions, so they have access to all configured tools and MCP servers
231
+ - Scheduled jobs run in separate local CLI sessions, so they have access to all configured tools and MCP servers
232
232
 
233
233
  ## Troubleshooting
234
234
 
@@ -237,7 +237,7 @@ The Gemini CLI is aware of the scheduler API through the system prompt. You can
237
237
  - Verify the cron expression is valid using a cron expression tester
238
238
  - Ensure the bridge is running continuously
239
239
 
240
- ### Results not appearing in Telegram
240
+ ### Results not appearing in Telegram (current adapter)
241
241
  - Send at least one message to your bot first to establish the chat binding
242
242
  - Check if `lastIncomingChatId` is set in the logs
243
243
 
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',
@@ -23,7 +24,7 @@ const ENV_KEY_MAP: Record<string, string> = {
23
24
  callbackPort: 'CALLBACK_PORT',
24
25
  callbackAuthToken: 'CALLBACK_AUTH_TOKEN',
25
26
  callbackMaxBodyBytes: 'CALLBACK_MAX_BODY_BYTES',
26
- agentBridgeHome: 'AGENT_BRIDGE_HOME',
27
+ ClawlessHome: 'AGENT_BRIDGE_HOME',
27
28
  memoryFilePath: 'MEMORY_FILE_PATH',
28
29
  memoryMaxChars: 'MEMORY_MAX_CHARS',
29
30
  schedulesFilePath: 'SCHEDULES_FILE_PATH',
@@ -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',
@@ -50,7 +52,7 @@ const DEFAULT_CONFIG_TEMPLATE = {
50
52
  callbackPort: 8788,
51
53
  callbackAuthToken: '',
52
54
  callbackMaxBodyBytes: 65536,
53
- agentBridgeHome: '~/.clawless',
55
+ ClawlessHome: '~/.clawless',
54
56
  memoryFilePath: '~/.clawless/MEMORY.md',
55
57
  memoryMaxChars: 12000,
56
58
  schedulesFilePath: '~/.clawless/schedules.json',
@@ -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
 
@@ -576,9 +612,9 @@ function ensureMemoryFile() {
576
612
 
577
613
  if (!fs.existsSync(MEMORY_FILE_PATH)) {
578
614
  const template = [
579
- '# Gemini Bridge Memory',
615
+ '# Clawless Memory',
580
616
  '',
581
- 'This file stores durable memory notes for Gemini Bridge.',
617
+ 'This file stores durable memory notes for Clawless.',
582
618
  '',
583
619
  '## Notes',
584
620
  '',
@@ -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.0",
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",
@@ -1,226 +0,0 @@
1
- # Quick Start Guide: Scheduler Feature
2
-
3
- This guide will help you start using the scheduler feature in under 5 minutes.
4
-
5
- ## Prerequisites
6
-
7
- 1. Clawless is installed and configured
8
- 2. You have sent at least one message to your bot (to establish chat binding)
9
- 3. The bridge is running (`npm run dev` or `npm start`)
10
-
11
- ## Basic Usage
12
-
13
- ### Talk to Gemini Naturally
14
-
15
- The easiest way to use the scheduler is to just ask Gemini:
16
-
17
- **Examples:**
18
-
19
- ```
20
- You: "Remind me to take a break in 30 minutes"
21
-
22
- You: "Check my calendar every morning at 9am and send me a summary"
23
-
24
- You: "Every Friday at 5pm, remind me to review my weekly goals"
25
-
26
- You: "What schedules do I have?"
27
-
28
- You: "Cancel the daily calendar check"
29
- ```
30
-
31
- Gemini will handle all the API calls automatically!
32
-
33
- ## Direct API Usage
34
-
35
- ### 1. Create a Recurring Schedule
36
-
37
- ```bash
38
- curl -X POST http://127.0.0.1:8788/api/schedule \
39
- -H "Content-Type: application/json" \
40
- -d '{
41
- "message": "What time is it?",
42
- "description": "Time check",
43
- "cronExpression": "0 9 * * *"
44
- }'
45
- ```
46
-
47
- **Response:**
48
- ```json
49
- {
50
- "ok": true,
51
- "schedule": {
52
- "id": "schedule_1707835800000_abc123",
53
- "message": "What time is it?",
54
- "description": "Time check",
55
- "cronExpression": "0 9 * * *",
56
- "oneTime": false,
57
- "active": true,
58
- "createdAt": "2026-02-13T10:00:00.000Z"
59
- }
60
- }
61
- ```
62
-
63
- ### 2. Create a One-Time Schedule
64
-
65
- ```bash
66
- # Schedule for 30 seconds from now
67
- if ! RUN_AT=$(date -u -d "+30 seconds" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null); then
68
- # Fallback for macOS/BSD date
69
- RUN_AT=$(date -u -v+30S +"%Y-%m-%dT%H:%M:%SZ")
70
- fi
71
-
72
- curl -X POST http://127.0.0.1:8788/api/schedule \
73
- -H "Content-Type: application/json" \
74
- -d "{
75
- \"message\": \"Test reminder\",
76
- \"oneTime\": true,
77
- \"runAt\": \"${RUN_AT}\"
78
- }"
79
- ```
80
-
81
- ### 3. List All Schedules
82
-
83
- ```bash
84
- curl http://127.0.0.1:8788/api/schedule | jq .
85
- ```
86
-
87
- ### 4. Delete a Schedule
88
-
89
- ```bash
90
- curl -X DELETE http://127.0.0.1:8788/api/schedule/SCHEDULE_ID
91
- ```
92
-
93
- ## Common Cron Expressions
94
-
95
- | Expression | Meaning |
96
- |------------|---------|
97
- | `0 9 * * *` | Every day at 9:00 AM |
98
- | `0 */6 * * *` | Every 6 hours |
99
- | `*/30 * * * *` | Every 30 minutes |
100
- | `0 9 * * 1-5` | Weekdays at 9:00 AM |
101
- | `0 17 * * 5` | Every Friday at 5:00 PM |
102
- | `0 0 1 * *` | First day of month at midnight |
103
-
104
- **Cron format:** `minute hour day month weekday`
105
-
106
- ## What Happens When a Job Runs?
107
-
108
- 1. **Scheduler triggers** at the scheduled time
109
- 2. **Message is sent** to a new Gemini CLI session
110
- 3. **Gemini processes** the message (can use tools, files, etc.)
111
- 4. **Response is sent** to your Telegram chat automatically
112
-
113
- Example Telegram message you'll receive:
114
- ```
115
- 🔔 Scheduled task completed:
116
-
117
- Daily calendar summary
118
-
119
- Today's events:
120
- - 9:00 AM: Team standup
121
- - 2:00 PM: Project review
122
- - 4:30 PM: 1-on-1 with manager
123
- ```
124
-
125
- ## Testing Your First Schedule
126
-
127
- 1. **Start the bridge:**
128
- ```bash
129
- npm run dev
130
- ```
131
-
132
- 2. **Create a test schedule (runs in 30 seconds):**
133
- ```bash
134
- if ! RUN_AT=$(date -u -d "+30 seconds" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null); then
135
- # Fallback for macOS/BSD date
136
- RUN_AT=$(date -u -v+30S +"%Y-%m-%dT%H:%M:%SZ")
137
- fi
138
-
139
- curl -X POST http://127.0.0.1:8788/api/schedule \
140
- -H "Content-Type: application/json" \
141
- -d "{
142
- \"message\": \"Hello! This is a test scheduled message.\",
143
- \"description\": \"Test\",
144
- \"oneTime\": true,
145
- \"runAt\": \"${RUN_AT}\"
146
- }"
147
- ```
148
-
149
- 3. **Wait 30 seconds** - You should receive a message in Telegram!
150
-
151
- 4. **Or ask Gemini directly:**
152
- ```
153
- Send me a test message in 30 seconds
154
- ```
155
-
156
- ## With Authentication
157
-
158
- If you have `CALLBACK_AUTH_TOKEN` set in your config:
159
-
160
- ```bash
161
- curl -X POST http://127.0.0.1:8788/api/schedule \
162
- -H "Content-Type: application/json" \
163
- -H "x-callback-token: YOUR_TOKEN_HERE" \
164
- -d '{
165
- "message": "Test",
166
- "cronExpression": "0 9 * * *"
167
- }'
168
- ```
169
-
170
- ## Troubleshooting
171
-
172
- ### "No target chat available"
173
- **Solution:** Send at least one message to your bot first.
174
-
175
- ### Schedule not executing
176
- **Solution:**
177
- - Check bridge logs for errors
178
- - Verify cron expression is valid
179
- - Ensure bridge is still running
180
-
181
- ### Can't reach API
182
- **Solution:**
183
- - Verify bridge is running: `curl http://127.0.0.1:8788/healthz`
184
- - Check if port 8788 is available
185
- - Look for errors in bridge logs
186
-
187
- ## Next Steps
188
-
189
- - Read [SCHEDULER.md](SCHEDULER.md) for complete API documentation
190
- - Read [TESTING.md](TESTING.md) for comprehensive testing guide
191
- - Run `./scripts/test-scheduler.sh` for automated tests
192
- - View `./scripts/gemini-scheduler-examples.sh` for more examples
193
-
194
- ## Pro Tips
195
-
196
- 1. **Descriptions matter**: Add clear descriptions to help you remember what each schedule does
197
- 2. **Test with one-time first**: Before setting up recurring jobs, test with a one-time schedule
198
- 3. **Use natural language**: Just ask Gemini - it's easier than curl commands!
199
- 4. **Check regularly**: Use "What schedules do I have?" to review active schedules
200
- 5. **Timezone aware**: Set `TZ` environment variable if needed (default is UTC)
201
-
202
- ## Example Workflows
203
-
204
- ### Daily Morning Routine
205
- ```
206
- You: "Every morning at 7am, check my calendar and the weather, then send me a summary"
207
- ```
208
-
209
- ### Work Break Reminders
210
- ```
211
- You: "Remind me to take a break every 2 hours during work days"
212
- ```
213
-
214
- ### Weekly Review
215
- ```
216
- You: "Every Sunday at 6pm, remind me to plan my goals for next week"
217
- ```
218
-
219
- ### Custom Notifications
220
- ```
221
- You: "Check if there are any urgent emails every 30 minutes and notify me if found"
222
- ```
223
-
224
- ---
225
-
226
- That's it! You're ready to start scheduling with Clawless. 🎉
package/QUICKSTART.md DELETED
@@ -1,98 +0,0 @@
1
- # Quick Start Guide
2
-
3
- Get your Telegram-Gemini bridge running in 5 minutes!
4
-
5
- ## Prerequisites Checklist
6
-
7
- - [ ] Node.js 18+ installed (`node --version`)
8
- - [ ] Gemini CLI installed (`gemini --version`)
9
- - [ ] Telegram bot token from [@BotFather](https://t.me/BotFather)
10
-
11
- ## Setup Steps
12
-
13
- ### 1. Install Dependencies
14
- ```bash
15
- npm install
16
- ```
17
-
18
- ### 2. Configure Environment
19
- ```bash
20
- cp .env.example .env
21
- ```
22
-
23
- Edit `.env` and add your bot token:
24
- ```env
25
- TELEGRAM_TOKEN=1234567890:ABCdefGHIjklMNOpqrsTUVwxyz
26
- ```
27
-
28
- ### 3. Test Gemini CLI
29
- Verify Gemini CLI supports ACP:
30
- ```bash
31
- gemini --protocol acp
32
- ```
33
-
34
- ### 4. Run the Bridge
35
-
36
- **Quick test:**
37
- ```bash
38
- npm start
39
- ```
40
-
41
- **Development (auto-restart):**
42
- ```bash
43
- npm run dev
44
- ```
45
-
46
- **Production (with PM2):**
47
- ```bash
48
- npm install -g pm2
49
- pm2 start ecosystem.config.json
50
- pm2 logs
51
- ```
52
-
53
- ## Verify It's Working
54
-
55
- 1. Open Telegram
56
- 2. Find your bot (search for the username you gave it)
57
- 3. Send a message: "Hello!"
58
- 4. You should see "🤔 Thinking..." followed by Gemini's response
59
-
60
- ## Common Issues
61
-
62
- ### "TELEGRAM_TOKEN is required"
63
- - Check your `.env` file exists
64
- - Verify the token is on the line `TELEGRAM_TOKEN=...`
65
- - No quotes needed around the token
66
-
67
- ### "command not found: gemini"
68
- - Install Gemini CLI first
69
- - Verify with: `which gemini`
70
-
71
- ### Bot doesn't respond
72
- - Check logs: `pm2 logs` (if using PM2)
73
- - Or check console output
74
- - Verify your bot token is correct
75
-
76
- ### Rate limit errors (429)
77
- - Increase `UPDATE_INTERVAL_MS` in `.env` to 2000 or higher
78
- - Restart the bot
79
-
80
- ## Next Steps
81
-
82
- - Read the full [README.md](README.md) for detailed configuration
83
- - Configure MCP servers for tool use
84
- - Set up auto-start with PM2
85
-
86
- ## Getting Help
87
-
88
- - Check [README.md](README.md) troubleshooting section
89
- - Review Gemini CLI documentation
90
- - Open an issue on GitHub
91
-
92
- ---
93
-
94
- **Pro tip:** Keep the bridge running with PM2 and set it to auto-start on boot:
95
- ```bash
96
- pm2 startup
97
- pm2 save
98
- ```