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.
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Notification hook for Claude Code.
4
+ // Detects parent TTY, reads hook input from stdin, fires POST to daemon, exits.
5
+
6
+ const http = require('http');
7
+ const { execSync } = require('child_process');
8
+
9
+ const DAEMON_PORT = 7483;
10
+ const DAEMON_HOST = '127.0.0.1';
11
+
12
+ function readStdin() {
13
+ return new Promise((resolve, reject) => {
14
+ let data = '';
15
+ process.stdin.setEncoding('utf8');
16
+ process.stdin.on('data', (chunk) => { data += chunk; });
17
+ process.stdin.on('end', () => {
18
+ try { resolve(JSON.parse(data)); }
19
+ catch (e) { reject(e); }
20
+ });
21
+ process.stdin.on('error', reject);
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Walk up the process tree to find the TTY of the Claude process.
27
+ * Hook process itself has tty=??, but the Claude parent has a real TTY.
28
+ */
29
+ function findTty() {
30
+ try {
31
+ let pid = process.ppid;
32
+ for (let i = 0; i < 10; i++) {
33
+ const tty = execSync(`ps -o tty= -p ${pid} 2>/dev/null`).toString().trim();
34
+ if (tty && tty !== '??' && tty !== '') {
35
+ return `/dev/${tty}`;
36
+ }
37
+ const ppid = execSync(`ps -o ppid= -p ${pid} 2>/dev/null`).toString().trim();
38
+ if (!ppid || ppid === '0' || ppid === '1') break;
39
+ pid = parseInt(ppid);
40
+ }
41
+ } catch {}
42
+ return null;
43
+ }
44
+
45
+ function postToDaemon(body) {
46
+ return new Promise((resolve, reject) => {
47
+ const payload = JSON.stringify(body);
48
+ const req = http.request(
49
+ {
50
+ hostname: DAEMON_HOST,
51
+ port: DAEMON_PORT,
52
+ path: '/api/notify',
53
+ method: 'POST',
54
+ headers: {
55
+ 'Content-Type': 'application/json',
56
+ 'Content-Length': Buffer.byteLength(payload),
57
+ },
58
+ timeout: 5000,
59
+ },
60
+ (res) => {
61
+ res.on('data', () => {});
62
+ res.on('end', resolve);
63
+ }
64
+ );
65
+ req.on('error', reject);
66
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
67
+ req.write(payload);
68
+ req.end();
69
+ });
70
+ }
71
+
72
+ async function main() {
73
+ try {
74
+ const input = await readStdin();
75
+ const hookInput = input.hookInput || input;
76
+ const ttyPath = findTty();
77
+
78
+ await postToDaemon({
79
+ session_id: hookInput.session_id,
80
+ cwd: hookInput.cwd,
81
+ notification_type: hookInput.notification_type || hookInput.type,
82
+ message: hookInput.message,
83
+ transcript_path: hookInput.transcript_path,
84
+ tty_path: ttyPath,
85
+ });
86
+ } catch {
87
+ // Daemon unreachable — silently ignore
88
+ }
89
+ process.exit(0);
90
+ }
91
+
92
+ main();
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+
3
+ // PermissionRequest hook for Claude Code.
4
+ // Reads hook input from stdin, forwards to daemon, blocks until Telegram response.
5
+ // On error/timeout: exits 0 with no output → falls back to local dialog.
6
+
7
+ const http = require('http');
8
+ const { execSync } = require('child_process');
9
+
10
+ const DAEMON_PORT = 7483;
11
+ const DAEMON_HOST = '127.0.0.1';
12
+
13
+ function findTty() {
14
+ try {
15
+ let pid = process.ppid;
16
+ for (let i = 0; i < 10; i++) {
17
+ const tty = execSync(`ps -o tty= -p ${pid} 2>/dev/null`).toString().trim();
18
+ if (tty && tty !== '??' && tty !== '') {
19
+ return `/dev/${tty}`;
20
+ }
21
+ const ppid = execSync(`ps -o ppid= -p ${pid} 2>/dev/null`).toString().trim();
22
+ if (!ppid || ppid === '0' || ppid === '1') break;
23
+ pid = parseInt(ppid);
24
+ }
25
+ } catch {}
26
+ return null;
27
+ }
28
+
29
+ function readStdin() {
30
+ return new Promise((resolve, reject) => {
31
+ let data = '';
32
+ process.stdin.setEncoding('utf8');
33
+ process.stdin.on('data', (chunk) => { data += chunk; });
34
+ process.stdin.on('end', () => {
35
+ try { resolve(JSON.parse(data)); }
36
+ catch (e) { reject(e); }
37
+ });
38
+ process.stdin.on('error', reject);
39
+ });
40
+ }
41
+
42
+ function postToDaemon(path, body) {
43
+ return new Promise((resolve, reject) => {
44
+ const payload = JSON.stringify(body);
45
+ const req = http.request(
46
+ {
47
+ hostname: DAEMON_HOST,
48
+ port: DAEMON_PORT,
49
+ path,
50
+ method: 'POST',
51
+ headers: {
52
+ 'Content-Type': 'application/json',
53
+ 'Content-Length': Buffer.byteLength(payload),
54
+ },
55
+ timeout: 1800000, // 30 minutes — daemon holds until Telegram answer
56
+ },
57
+ (res) => {
58
+ let data = '';
59
+ res.on('data', (chunk) => { data += chunk; });
60
+ res.on('end', () => {
61
+ try { resolve(JSON.parse(data)); }
62
+ catch { resolve(null); }
63
+ });
64
+ }
65
+ );
66
+ req.on('error', reject);
67
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
68
+ req.write(payload);
69
+ req.end();
70
+ });
71
+ }
72
+
73
+ async function main() {
74
+ try {
75
+ const input = await readStdin();
76
+
77
+ const hookInput = input.hookInput || input;
78
+ const ttyPath = findTty();
79
+ const result = await postToDaemon('/api/permission', {
80
+ session_id: hookInput.session_id,
81
+ cwd: hookInput.cwd,
82
+ tool_name: hookInput.tool_name,
83
+ tool_input: hookInput.tool_input,
84
+ permission_suggestions: hookInput.permission_suggestions,
85
+ transcript_path: hookInput.transcript_path,
86
+ tty_path: ttyPath,
87
+ });
88
+
89
+ if (!result || !result.decision) {
90
+ // No decision — fall back to local dialog
91
+ process.exit(0);
92
+ }
93
+
94
+ const output = {
95
+ hookSpecificOutput: {
96
+ hookEventName: 'PermissionRequest',
97
+ decision: result.decision,
98
+ },
99
+ statusMessage: 'Waiting for Telegram approval...',
100
+ };
101
+
102
+ process.stdout.write(JSON.stringify(output));
103
+ } catch {
104
+ // Daemon unreachable or error — fall back to local dialog
105
+ process.exit(0);
106
+ }
107
+ }
108
+
109
+ main();
package/src/setup.js ADDED
@@ -0,0 +1,221 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const readline = require('readline');
4
+ const https = require('https');
5
+ const { loadConfig, saveConfig } = require('./config');
6
+
7
+ const CLAUDE_SETTINGS_PATH = path.join(process.env.HOME, '.claude', 'settings.json');
8
+ const HOOKS_DIR = path.resolve(__dirname, 'hooks');
9
+
10
+ function ask(rl, question) {
11
+ return new Promise((resolve) => rl.question(question, resolve));
12
+ }
13
+
14
+ function telegramApiCall(token, method) {
15
+ return new Promise((resolve, reject) => {
16
+ https.get(`https://api.telegram.org/bot${token}/${method}`, (res) => {
17
+ let data = '';
18
+ res.on('data', (chunk) => { data += chunk; });
19
+ res.on('end', () => {
20
+ try { resolve(JSON.parse(data)); }
21
+ catch (e) { reject(e); }
22
+ });
23
+ }).on('error', reject);
24
+ });
25
+ }
26
+
27
+ function getHooksConfig(port) {
28
+ return {
29
+ PermissionRequest: [
30
+ {
31
+ hooks: [
32
+ {
33
+ type: 'command',
34
+ command: `node ${path.join(HOOKS_DIR, 'permission-request.js')}`,
35
+ timeout: 1800,
36
+ statusMessage: 'Waiting for Telegram approval...',
37
+ },
38
+ ],
39
+ },
40
+ ],
41
+ Notification: [
42
+ {
43
+ matcher: 'idle_prompt|elicitation_dialog',
44
+ hooks: [
45
+ {
46
+ type: 'command',
47
+ command: `node ${path.join(HOOKS_DIR, 'notification.js')}`,
48
+ async: true,
49
+ },
50
+ ],
51
+ },
52
+ ],
53
+ };
54
+ }
55
+
56
+ function installHooks(port) {
57
+ let settings = {};
58
+ if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
59
+ try {
60
+ settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8'));
61
+ } catch {
62
+ settings = {};
63
+ }
64
+ }
65
+
66
+ if (!settings.hooks) settings.hooks = {};
67
+
68
+ const newHooks = getHooksConfig(port);
69
+
70
+ // Replace our hooks (identified by our hook command paths), preserve others
71
+ for (const [event, hookConfigs] of Object.entries(newHooks)) {
72
+ const existing = settings.hooks[event] || [];
73
+ // Remove any previous telegram-bridge hooks
74
+ const filtered = existing.filter((entry) => {
75
+ const hooks = entry.hooks || [];
76
+ return !hooks.some((h) => h.command && h.command.includes('claude-telegram-bridge'));
77
+ });
78
+ settings.hooks[event] = [...filtered, ...hookConfigs];
79
+ }
80
+
81
+ // Ensure directory exists
82
+ const dir = path.dirname(CLAUDE_SETTINGS_PATH);
83
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
84
+
85
+ fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
86
+ }
87
+
88
+ function uninstallHooks() {
89
+ if (!fs.existsSync(CLAUDE_SETTINGS_PATH)) return;
90
+
91
+ let settings;
92
+ try {
93
+ settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8'));
94
+ } catch {
95
+ return;
96
+ }
97
+
98
+ if (!settings.hooks) return;
99
+
100
+ for (const event of Object.keys(settings.hooks)) {
101
+ settings.hooks[event] = (settings.hooks[event] || []).filter((entry) => {
102
+ const hooks = entry.hooks || [];
103
+ return !hooks.some((h) => h.command && h.command.includes('claude-telegram-bridge'));
104
+ });
105
+ if (settings.hooks[event].length === 0) {
106
+ delete settings.hooks[event];
107
+ }
108
+ }
109
+
110
+ if (Object.keys(settings.hooks).length === 0) {
111
+ delete settings.hooks;
112
+ }
113
+
114
+ fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
115
+ }
116
+
117
+ async function run() {
118
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
119
+ const config = loadConfig();
120
+
121
+ console.log('\nClaude Telegram Bridge Setup\n');
122
+
123
+ // Bot token
124
+ const token = await ask(rl, `Telegram Bot Token${config.botToken ? ' [keep existing]' : ''}: `);
125
+ if (token.trim()) {
126
+ config.botToken = token.trim();
127
+ }
128
+ if (!config.botToken) {
129
+ console.error('Bot token is required. Create one via @BotFather on Telegram.');
130
+ rl.close();
131
+ process.exit(1);
132
+ }
133
+
134
+ // Validate token
135
+ console.log('Validating bot token...');
136
+ try {
137
+ const me = await telegramApiCall(config.botToken, 'getMe');
138
+ if (!me.ok) throw new Error(me.description);
139
+ console.log(`Bot: @${me.result.username}`);
140
+ } catch (err) {
141
+ console.error(`Invalid bot token: ${err.message}`);
142
+ rl.close();
143
+ process.exit(1);
144
+ }
145
+
146
+ // Chat ID
147
+ if (!config.chatId) {
148
+ console.log('\nSend /start to your bot in Telegram, then press Enter here...');
149
+ await ask(rl, 'Press Enter after sending /start to the bot: ');
150
+
151
+ const updates = await telegramApiCall(config.botToken, 'getUpdates?offset=-1');
152
+ if (updates.ok && updates.result.length > 0) {
153
+ const lastUpdate = updates.result[updates.result.length - 1];
154
+ const chat = lastUpdate.message?.chat;
155
+ if (chat) {
156
+ config.chatId = chat.id.toString();
157
+ console.log(`Chat ID captured: ${config.chatId}`);
158
+ }
159
+ }
160
+
161
+ if (!config.chatId) {
162
+ const manual = await ask(rl, 'Could not auto-detect. Enter Chat ID manually: ');
163
+ config.chatId = manual.trim();
164
+ }
165
+ } else {
166
+ console.log(`Using existing Chat ID: ${config.chatId}`);
167
+ const change = await ask(rl, 'Change Chat ID? [y/N]: ');
168
+ if (change.toLowerCase() === 'y') {
169
+ console.log('Send /start to your bot in Telegram, then press Enter here...');
170
+ await ask(rl, 'Press Enter after sending /start to the bot: ');
171
+ const updates = await telegramApiCall(config.botToken, 'getUpdates?offset=-1');
172
+ if (updates.ok && updates.result.length > 0) {
173
+ const lastUpdate = updates.result[updates.result.length - 1];
174
+ const chat = lastUpdate.message?.chat;
175
+ if (chat) {
176
+ config.chatId = chat.id.toString();
177
+ console.log(`Chat ID updated: ${config.chatId}`);
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ if (!config.chatId) {
184
+ console.error('Chat ID is required.');
185
+ rl.close();
186
+ process.exit(1);
187
+ }
188
+
189
+ // Port
190
+ const portStr = await ask(rl, `Daemon port [${config.port}]: `);
191
+ if (portStr.trim()) {
192
+ config.port = parseInt(portStr.trim(), 10);
193
+ }
194
+
195
+ // Save config
196
+ saveConfig(config);
197
+ console.log('\nConfig saved.');
198
+
199
+ // Install hooks
200
+ installHooks(config.port);
201
+ console.log('Hooks installed into ~/.claude/settings.json');
202
+
203
+ // Send test message
204
+ console.log('Sending test message...');
205
+ try {
206
+ const { Telegraf } = require('telegraf');
207
+ const testBot = new Telegraf(config.botToken);
208
+ await testBot.telegram.sendMessage(config.chatId, '✅ Claude Telegram Bridge configured successfully!\n\nRun `claude-tg daemon start` to begin.');
209
+ console.log('Test message sent to Telegram.');
210
+ } catch (err) {
211
+ console.error(`Could not send test message: ${err.message}`);
212
+ }
213
+
214
+ console.log('\nSetup complete! Next steps:');
215
+ console.log(' claude-tg daemon start — Start the daemon');
216
+ console.log(' claude — Use Claude as normal\n');
217
+
218
+ rl.close();
219
+ }
220
+
221
+ module.exports = { run, installHooks, uninstallHooks };