claude-tg 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Himanshu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,283 @@
1
+ # claude-tg
2
+
3
+ Control Claude Code from your phone via Telegram — approve permissions, answer interactive questions, reply to idle sessions, and send files.
4
+
5
+ If you run Claude CLI and step away from your machine, it stalls whenever it needs tool permission or asks a question. This bridge sends everything to Telegram so you can keep Claude working remotely.
6
+
7
+ ```
8
+ Claude CLI (any terminal) Daemon (background)
9
+ ┌─────────────────────┐ ┌─────────────────────┐
10
+ │ PermissionRequest │──HTTP──> │ Telegram Bot │──> Your Phone
11
+ │ hook (blocking) │<─────────│ HTTP Server (:7483) │<── (Telegram App)
12
+ └─────────────────────┘ └─────────────────────┘
13
+
14
+ Notification hook ──async HTTP──> Daemon ──> Telegram alert
15
+ <── You reply with text
16
+ Daemon ──> Types into terminal via osascript
17
+ ```
18
+
19
+ Uses Claude Code's native [hooks system](https://docs.anthropic.com/en/docs/claude-code/hooks) — installs into `~/.claude/settings.json` and applies to all Claude instances automatically. No PTY wrappers or hacks.
20
+
21
+ ## Features
22
+
23
+ - **Remote permission approval** — Allow, Deny, or Always Allow tool calls from Telegram inline buttons
24
+ - **Interactive questions** — When Claude uses `AskUserQuestion`, you see the actual options as Telegram buttons — pick answers, type custom responses, review a summary, then confirm or redo
25
+ - **Rich context** — Each message shows the session number, project name, original task, what Claude was doing, and the exact tool/command
26
+ - **Multi-session support** — Sessions are labeled #1, #2, #3... and persist as long as the terminal is open
27
+ - **Idle notifications** — Get alerted when Claude is waiting for your input, with its last message shown
28
+ - **Reply from Telegram** — Swipe-reply to a notification to send text input to the correct terminal (macOS)
29
+ - **Concurrent session routing** — Reply-to targets a specific session; auto-routes when only one is idle
30
+ - **Send messages & files** — Tell Claude "send this to my telegram" and it sends text, images, videos, documents, audio
31
+ - **Smart file handling** — Images display as photos, videos play inline, audio streams — not just generic document attachments
32
+ - **Graceful fallback** — If the daemon isn't running, hooks exit silently and Claude shows the normal local dialog
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ npm install -g claude-tg
38
+ ```
39
+
40
+ ## Setup
41
+
42
+ ### 1. Create a Telegram Bot
43
+
44
+ 1. Open Telegram and message [@BotFather](https://t.me/botfather)
45
+ 2. Send `/newbot` and follow the prompts
46
+ 3. Copy the bot token
47
+
48
+ ### 2. Run Setup
49
+
50
+ ```bash
51
+ claude-tg setup
52
+ ```
53
+
54
+ This will:
55
+ - Ask for your bot token (validates it against the Telegram API)
56
+ - Capture your chat ID (send `/start` to your bot when prompted)
57
+ - Install hooks into `~/.claude/settings.json` (merges with existing config)
58
+ - Save config to `~/.claude-telegram-bridge/config.json`
59
+ - Send a test message to your Telegram
60
+
61
+ ### 3. Start the Daemon
62
+
63
+ ```bash
64
+ claude-tg daemon start
65
+ ```
66
+
67
+ That's it. Use `claude` as normal — permission prompts and questions now go to Telegram.
68
+
69
+ ## Usage
70
+
71
+ ### Permission Requests
72
+
73
+ When Claude needs tool permission, you get a Telegram message:
74
+
75
+ ```
76
+ 📋 #2 my-project
77
+ ━━━━━━━━━━━━━━━━━━━━
78
+ 📝 Task: implement user authentication with JWT
79
+ 💭 Doing: Let me install the jsonwebtoken package...
80
+
81
+ 🔧 Bash
82
+ npm install jsonwebtoken
83
+
84
+ [Allow] [Deny] [Always Allow]
85
+ ```
86
+
87
+ - **Allow** — permits this one tool call
88
+ - **Deny** — blocks the tool call
89
+ - **Always Allow** — permits and adds a rule so future calls of this type don't ask
90
+
91
+ ### Interactive Questions
92
+
93
+ When Claude asks you questions (via `AskUserQuestion`), you see the actual options as buttons:
94
+
95
+ ```
96
+ 💬 #2 my-project — Claude has a question
97
+ ━━━━━━━━━━━━━━━━━━━━
98
+ 📝 Task: build a web app
99
+
100
+ ❓ [1/2] Which auth approach?
101
+
102
+ • NextAuth.js: Built-in Next.js auth solution
103
+ • Clerk: Managed auth service
104
+ • Supabase Auth: Open-source auth
105
+
106
+ [NextAuth.js] [Clerk]
107
+ [Supabase Auth] [✏️ Custom]
108
+ ```
109
+
110
+ - Tap an option to select it and move to the next question
111
+ - Tap **Custom** to type a free-text answer
112
+ - For multi-select questions, tap multiple options then **Next**
113
+ - After all questions, review your answers and tap **Confirm** or **Redo**
114
+
115
+ Your answers are automatically injected into the terminal via keystrokes.
116
+
117
+ ### Idle Notifications
118
+
119
+ When Claude finishes and waits for input:
120
+
121
+ ```
122
+ ⏳ #2 my-project — Claude is idle
123
+ ━━━━━━━━━━━━━━━━━━━━
124
+ 📝 Task: implement user authentication with JWT
125
+ 💬 Claude said:
126
+ I've set up the JWT middleware. What would you like me to work on next?
127
+
128
+ ↩️ Reply to this message to send input
129
+ ```
130
+
131
+ **Swipe-reply** to this message in Telegram with your next instruction — it gets typed into the correct terminal and submitted.
132
+
133
+ ### Multiple Sessions
134
+
135
+ Each Claude session gets a persistent label (#1, #2, #3...) that lasts as long as the terminal stays open. When multiple sessions are idle and you send a plain message, the bot asks you to reply to a specific notification:
136
+
137
+ ```
138
+ Multiple sessions are waiting. Reply to a specific notification message to choose:
139
+
140
+ #1 saas-factory
141
+ #3 ml-pipeline
142
+ ```
143
+
144
+ If only one session is idle, your text is auto-routed.
145
+
146
+ ### Sending Messages & Files
147
+
148
+ Tell Claude "send this to my telegram" or "send this file to my telegram". It uses these commands:
149
+
150
+ ```bash
151
+ # Send a text message
152
+ claude-tg send "Here's the summary you asked for..."
153
+
154
+ # Send a file (images, videos, audio, documents)
155
+ claude-tg send-file ./screenshot.png "Latest UI"
156
+ claude-tg send-file ./demo.mp4 "Feature demo"
157
+ claude-tg send-file ./report.pdf "Monthly report"
158
+
159
+ # Pipe content from stdin
160
+ echo "hello" | claude-tg send -
161
+ ```
162
+
163
+ These work independently of the daemon — they hit the Telegram API directly.
164
+
165
+ Files are sent using the correct Telegram method based on type:
166
+ | Extension | Sent as |
167
+ |---|---|
168
+ | `.jpg` `.jpeg` `.png` `.webp` | Photo (with preview) |
169
+ | `.mp4` `.mov` `.avi` `.mkv` `.webm` | Video (plays inline) |
170
+ | `.gif` | Animation |
171
+ | `.mp3` `.ogg` `.wav` `.flac` `.m4a` `.aac` | Audio (streams) |
172
+ | Everything else | Document |
173
+
174
+ For long text (>4096 chars), `claude-tg send` automatically sends it as a `.md` document.
175
+
176
+ ### Bot Commands
177
+
178
+ - `/status` — list active sessions, pending permissions, and pending questions
179
+ - `/start` — register chat ID (used during setup)
180
+
181
+ ## CLI Reference
182
+
183
+ ```
184
+ claude-tg setup # Interactive setup
185
+ claude-tg daemon start # Start background daemon
186
+ claude-tg daemon stop # Stop daemon
187
+ claude-tg daemon status # Check daemon status + pending requests
188
+ claude-tg daemon logs # Tail daemon logs
189
+ claude-tg send <text> # Send text message (use "-" for stdin)
190
+ claude-tg send-file <path> # Send a file (optional caption as 2nd arg)
191
+ claude-tg uninstall # Remove hooks from ~/.claude/settings.json
192
+ ```
193
+
194
+ ## How It Works
195
+
196
+ ### Hooks
197
+
198
+ Claude Code supports [hooks](https://docs.anthropic.com/en/docs/claude-code/hooks) — shell commands that run in response to lifecycle events. Two hooks are installed:
199
+
200
+ **PermissionRequest** (blocking) — Fires when Claude needs tool permission. The hook:
201
+ 1. Reads the hook input from stdin (session ID, tool name, tool input, transcript path)
202
+ 2. Detects the parent Claude process's TTY (for reply routing)
203
+ 3. POSTs to the local daemon
204
+ 4. Blocks until the daemon responds (which waits for your Telegram tap)
205
+ 5. Returns the decision as JSON to stdout
206
+
207
+ When `AskUserQuestion` is the tool, the daemon shows the actual questions as Telegram buttons instead of Allow/Deny. After you answer, it allows the tool and injects your selections into the terminal.
208
+
209
+ **Notification** (async) — Fires on `idle_prompt` and `elicitation_dialog`. The hook:
210
+ 1. Reads the hook input from stdin
211
+ 2. Detects the parent TTY
212
+ 3. Fire-and-forget POST to the daemon
213
+ 4. Exits immediately
214
+
215
+ ### Daemon
216
+
217
+ A single background process on `localhost:7483`. Runs a Telegram bot (via telegraf) and an HTTP server.
218
+
219
+ - `POST /api/permission` — Holds the HTTP connection open until you respond on Telegram
220
+ - `POST /api/notify` — Sends an alert and stores the session for reply routing
221
+ - `GET /api/health` — Health check with pending count
222
+
223
+ ### Session Tracking
224
+
225
+ Sessions are tracked by their `session_id` from Claude Code. Each session's TTY path is detected by walking the process tree. Sessions persist as long as the TTY has active processes — they don't expire on a timer.
226
+
227
+ ### Reply Injection (macOS)
228
+
229
+ When you reply to a notification from Telegram, the daemon uses `osascript` to type your text into the correct terminal:
230
+
231
+ - **iTerm2** — Uses `write text` on the session matched by TTY path. Works without bringing the window to front.
232
+ - **Terminal.app** — Finds the tab by TTY, focuses it, then uses System Events to keystroke the text + press Return.
233
+
234
+ For interactive questions, the daemon injects the answer sequence using keyboard navigation (arrow keys, space, tab, enter).
235
+
236
+ ### Graceful Degradation
237
+
238
+ If the daemon is not running:
239
+ - Hook scripts detect the connection failure and `exit 0` with no output
240
+ - Claude Code falls through to the normal local permission dialog
241
+ - Zero disruption — you just don't get Telegram notifications
242
+
243
+ ## Project Structure
244
+
245
+ ```
246
+ claude-tg/
247
+ ├── bin/
248
+ │ └── claude-tg # CLI entry point
249
+ ├── src/
250
+ │ ├── config.js # Read/write ~/.claude-telegram-bridge/config.json
251
+ │ ├── daemon.js # Telegram bot + HTTP server + session tracking
252
+ │ ├── setup.js # Interactive setup + hook installation
253
+ │ └── hooks/
254
+ │ ├── permission-request.js # Blocking PermissionRequest hook
255
+ │ └── notification.js # Async Notification hook
256
+ ├── package.json
257
+ └── README.md
258
+ ```
259
+
260
+ **Config directory:** `~/.claude-telegram-bridge/`
261
+ - `config.json` — bot token, chat ID, port
262
+ - `daemon.pid` — PID of running daemon
263
+ - `daemon.log` — daemon logs
264
+
265
+ ## Limitations
266
+
267
+ - **Reply from Telegram** requires macOS with Terminal.app or iTerm2. On Linux or other terminals, you'll see notifications but need to respond in the terminal.
268
+ - **Interactive question injection** uses AppleScript keystrokes — requires the terminal to be accessible (not locked screen).
269
+ - **Session labels reset** when the daemon restarts (#1, #2... start over).
270
+ - **30-minute timeout** on permission requests. If you don't respond, the hook exits and Claude shows the local dialog.
271
+
272
+ ## Uninstalling
273
+
274
+ ```bash
275
+ claude-tg daemon stop
276
+ claude-tg uninstall
277
+ npm uninstall -g claude-tg
278
+ rm -rf ~/.claude-telegram-bridge
279
+ ```
280
+
281
+ ## License
282
+
283
+ MIT
package/bin/claude-tg ADDED
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Command } = require('commander');
4
+ const { spawn, execSync } = require('child_process');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { loadConfig, PID_PATH, LOG_PATH, CONFIG_DIR } = require('../src/config');
8
+
9
+ const program = new Command();
10
+
11
+ program
12
+ .name('claude-tg')
13
+ .description('Claude Code ↔ Telegram Bridge')
14
+ .version('1.0.0');
15
+
16
+ program
17
+ .command('setup')
18
+ .description('Interactive setup: bot token, chat ID, hook installation')
19
+ .action(async () => {
20
+ const { run } = require('../src/setup');
21
+ await run();
22
+ });
23
+
24
+ const daemon = program.command('daemon').description('Manage the background daemon');
25
+
26
+ daemon
27
+ .command('start')
28
+ .description('Start the daemon as a background process')
29
+ .action(() => {
30
+ const config = loadConfig();
31
+ if (!config.botToken || !config.chatId) {
32
+ console.error('Not configured. Run: claude-tg setup');
33
+ process.exit(1);
34
+ }
35
+
36
+ // Check if already running
37
+ if (fs.existsSync(PID_PATH)) {
38
+ const pid = parseInt(fs.readFileSync(PID_PATH, 'utf8').trim(), 10);
39
+ try {
40
+ process.kill(pid, 0); // Check if process exists
41
+ console.log(`Daemon already running (PID ${pid})`);
42
+ return;
43
+ } catch {
44
+ // PID file stale, remove it
45
+ fs.unlinkSync(PID_PATH);
46
+ }
47
+ }
48
+
49
+ // Ensure config dir exists
50
+ if (!fs.existsSync(CONFIG_DIR)) {
51
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
52
+ }
53
+
54
+ const logFd = fs.openSync(LOG_PATH, 'a');
55
+ const daemonPath = path.resolve(__dirname, '..', 'src', 'daemon.js');
56
+
57
+ const child = spawn('node', [daemonPath], {
58
+ detached: true,
59
+ stdio: ['ignore', logFd, logFd],
60
+ env: { ...process.env },
61
+ });
62
+
63
+ fs.writeFileSync(PID_PATH, child.pid.toString());
64
+ child.unref();
65
+ fs.closeSync(logFd);
66
+
67
+ console.log(`Daemon started (PID ${child.pid})`);
68
+ console.log(`Logs: ${LOG_PATH}`);
69
+ });
70
+
71
+ daemon
72
+ .command('stop')
73
+ .description('Stop the daemon')
74
+ .action(() => {
75
+ if (!fs.existsSync(PID_PATH)) {
76
+ console.log('Daemon is not running.');
77
+ return;
78
+ }
79
+ const pid = parseInt(fs.readFileSync(PID_PATH, 'utf8').trim(), 10);
80
+ try {
81
+ process.kill(pid, 'SIGTERM');
82
+ fs.unlinkSync(PID_PATH);
83
+ console.log(`Daemon stopped (PID ${pid})`);
84
+ } catch (err) {
85
+ if (err.code === 'ESRCH') {
86
+ fs.unlinkSync(PID_PATH);
87
+ console.log('Daemon was not running (stale PID file removed).');
88
+ } else {
89
+ console.error(`Failed to stop daemon: ${err.message}`);
90
+ }
91
+ }
92
+ });
93
+
94
+ daemon
95
+ .command('status')
96
+ .description('Check if daemon is running')
97
+ .action(() => {
98
+ if (!fs.existsSync(PID_PATH)) {
99
+ console.log('Daemon is not running.');
100
+ return;
101
+ }
102
+ const pid = parseInt(fs.readFileSync(PID_PATH, 'utf8').trim(), 10);
103
+ try {
104
+ process.kill(pid, 0);
105
+ console.log(`Daemon is running (PID ${pid})`);
106
+
107
+ // Try health check
108
+ const http = require('http');
109
+ const config = loadConfig();
110
+ const req = http.get(`http://127.0.0.1:${config.port}/api/health`, (res) => {
111
+ let data = '';
112
+ res.on('data', (chunk) => { data += chunk; });
113
+ res.on('end', () => {
114
+ try {
115
+ const health = JSON.parse(data);
116
+ console.log(`Pending requests: ${health.pending}`);
117
+ } catch {}
118
+ });
119
+ });
120
+ req.on('error', () => {});
121
+ req.setTimeout(2000, () => req.destroy());
122
+ } catch {
123
+ fs.unlinkSync(PID_PATH);
124
+ console.log('Daemon is not running (stale PID file removed).');
125
+ }
126
+ });
127
+
128
+ daemon
129
+ .command('logs')
130
+ .description('Tail daemon log file')
131
+ .action(() => {
132
+ if (!fs.existsSync(LOG_PATH)) {
133
+ console.log('No log file found.');
134
+ return;
135
+ }
136
+ try {
137
+ execSync(`tail -f ${LOG_PATH}`, { stdio: 'inherit' });
138
+ } catch {
139
+ // User pressed Ctrl+C
140
+ }
141
+ });
142
+
143
+ // --- Send commands (stateless, no daemon needed) ---
144
+
145
+ function createTelegramClient() {
146
+ const config = loadConfig();
147
+ if (!config.botToken || !config.chatId) {
148
+ console.error('Not configured. Run: claude-tg setup');
149
+ process.exit(1);
150
+ }
151
+ const { Telegram } = require('telegraf');
152
+ return { client: new Telegram(config.botToken), chatId: config.chatId };
153
+ }
154
+
155
+ function readAllStdin() {
156
+ return new Promise((resolve) => {
157
+ let data = '';
158
+ process.stdin.setEncoding('utf8');
159
+ process.stdin.on('data', (chunk) => { data += chunk; });
160
+ process.stdin.on('end', () => resolve(data));
161
+ if (process.stdin.isTTY) resolve('');
162
+ });
163
+ }
164
+
165
+ program
166
+ .command('send')
167
+ .argument('<text>', 'Text to send, or "-" to read from stdin')
168
+ .description('Send a text message to Telegram')
169
+ .action(async (text) => {
170
+ try {
171
+ if (text === '-') {
172
+ text = await readAllStdin();
173
+ }
174
+
175
+ if (!text.trim()) {
176
+ console.error('No text to send.');
177
+ process.exit(1);
178
+ }
179
+
180
+ const { client, chatId } = createTelegramClient();
181
+
182
+ if (text.length <= 4096) {
183
+ await client.sendMessage(chatId, text);
184
+ console.log('Message sent.');
185
+ } else {
186
+ // Long text — send as a document
187
+ const tmpFile = path.join(require('os').tmpdir(), `claude-tg-${Date.now()}.md`);
188
+ fs.writeFileSync(tmpFile, text);
189
+ const { Input } = require('telegraf');
190
+ await client.sendDocument(chatId, Input.fromLocalFile(tmpFile, 'message.md'), {
191
+ caption: `Message from Claude (${text.length} chars)`,
192
+ });
193
+ fs.unlinkSync(tmpFile);
194
+ console.log('Message sent as document.');
195
+ }
196
+ } catch (err) {
197
+ console.error(`Send failed: ${err.message}`);
198
+ process.exit(1);
199
+ }
200
+ });
201
+
202
+ program
203
+ .command('send-file')
204
+ .argument('<filepath>', 'Path to file')
205
+ .argument('[caption]', 'Optional caption')
206
+ .description('Send a file to Telegram')
207
+ .action(async (filepath, caption) => {
208
+ try {
209
+ const resolved = path.resolve(filepath);
210
+ if (!fs.existsSync(resolved)) {
211
+ console.error(`File not found: ${resolved}`);
212
+ process.exit(1);
213
+ }
214
+
215
+ const stat = fs.statSync(resolved);
216
+ if (stat.size > 50 * 1024 * 1024) {
217
+ console.error('File exceeds 50MB Telegram limit.');
218
+ process.exit(1);
219
+ }
220
+
221
+ const { client, chatId } = createTelegramClient();
222
+ const { Input } = require('telegraf');
223
+ const filename = path.basename(resolved);
224
+ const extra = caption ? { caption } : {};
225
+ const ext = path.extname(resolved).toLowerCase();
226
+
227
+ const photoExts = ['.jpg', '.jpeg', '.png', '.webp'];
228
+ const videoExts = ['.mp4', '.mov', '.avi', '.mkv', '.webm'];
229
+ const gifExts = ['.gif'];
230
+ const audioExts = ['.mp3', '.ogg', '.wav', '.flac', '.m4a', '.aac'];
231
+
232
+ const source = Input.fromLocalFile(resolved, filename);
233
+
234
+ if (photoExts.includes(ext)) {
235
+ await client.sendPhoto(chatId, source, extra);
236
+ } else if (videoExts.includes(ext)) {
237
+ await client.sendVideo(chatId, source, extra);
238
+ } else if (gifExts.includes(ext)) {
239
+ await client.sendAnimation(chatId, source, extra);
240
+ } else if (audioExts.includes(ext)) {
241
+ await client.sendAudio(chatId, source, extra);
242
+ } else {
243
+ await client.sendDocument(chatId, source, extra);
244
+ }
245
+ console.log(`File sent: ${filename}`);
246
+ } catch (err) {
247
+ console.error(`Send failed: ${err.message}`);
248
+ process.exit(1);
249
+ }
250
+ });
251
+
252
+ program
253
+ .command('uninstall')
254
+ .description('Remove hooks from ~/.claude/settings.json')
255
+ .action(() => {
256
+ const { uninstallHooks } = require('../src/setup');
257
+ uninstallHooks();
258
+ console.log('Hooks removed from ~/.claude/settings.json');
259
+ });
260
+
261
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "claude-tg",
3
+ "version": "1.0.0",
4
+ "description": "Control Claude Code from Telegram — approve permissions, answer questions, reply to idle sessions, send files, all from your phone",
5
+ "bin": {
6
+ "claude-tg": "./bin/claude-tg"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "src/",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "keywords": [
15
+ "claude",
16
+ "claude-code",
17
+ "telegram",
18
+ "telegram-bot",
19
+ "cli",
20
+ "hooks",
21
+ "remote",
22
+ "approval",
23
+ "permission",
24
+ "notification",
25
+ "multi-session"
26
+ ],
27
+ "author": "Himanshu",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/kokhp/claude-tg.git"
32
+ },
33
+ "homepage": "https://github.com/kokhp/claude-tg#readme",
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "dependencies": {
38
+ "telegraf": "^4.16.3",
39
+ "commander": "^12.0.0"
40
+ }
41
+ }
package/src/config.js ADDED
@@ -0,0 +1,38 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const CONFIG_DIR = path.join(process.env.HOME, '.claude-telegram-bridge');
5
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
6
+ const PID_PATH = path.join(CONFIG_DIR, 'daemon.pid');
7
+ const LOG_PATH = path.join(CONFIG_DIR, 'daemon.log');
8
+
9
+ const DEFAULT_CONFIG = {
10
+ botToken: '',
11
+ chatId: '',
12
+ port: 7483,
13
+ };
14
+
15
+ function ensureConfigDir() {
16
+ if (!fs.existsSync(CONFIG_DIR)) {
17
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
18
+ }
19
+ }
20
+
21
+ function loadConfig() {
22
+ ensureConfigDir();
23
+ if (!fs.existsSync(CONFIG_PATH)) {
24
+ return { ...DEFAULT_CONFIG };
25
+ }
26
+ try {
27
+ return { ...DEFAULT_CONFIG, ...JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')) };
28
+ } catch {
29
+ return { ...DEFAULT_CONFIG };
30
+ }
31
+ }
32
+
33
+ function saveConfig(config) {
34
+ ensureConfigDir();
35
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
36
+ }
37
+
38
+ module.exports = { loadConfig, saveConfig, CONFIG_DIR, CONFIG_PATH, PID_PATH, LOG_PATH };