claude-tg 1.0.6 → 1.0.8
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/bin/claude-tg.js +12 -6
- package/package.json +1 -1
- package/src/daemon.js +114 -41
package/bin/claude-tg.js
CHANGED
|
@@ -276,15 +276,21 @@ program
|
|
|
276
276
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
277
277
|
const hooks = settings.hooks || {};
|
|
278
278
|
|
|
279
|
+
// Extract script path from hook command (handles optional env prefix)
|
|
280
|
+
function extractScriptPath(cmd) {
|
|
281
|
+
const match = cmd.match(/node\s+(.+)$/);
|
|
282
|
+
return match ? match[1].trim() : null;
|
|
283
|
+
}
|
|
284
|
+
|
|
279
285
|
const permHooks = hooks.PermissionRequest || [];
|
|
280
286
|
const permCmd = permHooks[0]?.hooks?.[0]?.command || 'NOT FOUND';
|
|
281
287
|
console.log(` PermissionRequest: ${permCmd}`);
|
|
282
288
|
if (permCmd !== 'NOT FOUND') {
|
|
283
|
-
const scriptPath = permCmd
|
|
284
|
-
if (fs.existsSync(scriptPath)) {
|
|
289
|
+
const scriptPath = extractScriptPath(permCmd);
|
|
290
|
+
if (scriptPath && fs.existsSync(scriptPath)) {
|
|
285
291
|
console.log(' File exists: YES');
|
|
286
292
|
} else {
|
|
287
|
-
console.log(` File exists: NO — ${scriptPath}`);
|
|
293
|
+
console.log(` File exists: NO — ${scriptPath || permCmd}`);
|
|
288
294
|
ok = false;
|
|
289
295
|
}
|
|
290
296
|
}
|
|
@@ -293,11 +299,11 @@ program
|
|
|
293
299
|
const notifCmd = notifHooks[0]?.hooks?.[0]?.command || 'NOT FOUND';
|
|
294
300
|
console.log(` Notification: ${notifCmd}`);
|
|
295
301
|
if (notifCmd !== 'NOT FOUND') {
|
|
296
|
-
const scriptPath = notifCmd
|
|
297
|
-
if (fs.existsSync(scriptPath)) {
|
|
302
|
+
const scriptPath = extractScriptPath(notifCmd);
|
|
303
|
+
if (scriptPath && fs.existsSync(scriptPath)) {
|
|
298
304
|
console.log(' File exists: YES');
|
|
299
305
|
} else {
|
|
300
|
-
console.log(` File exists: NO — ${scriptPath}`);
|
|
306
|
+
console.log(` File exists: NO — ${scriptPath || notifCmd}`);
|
|
301
307
|
ok = false;
|
|
302
308
|
}
|
|
303
309
|
}
|
package/package.json
CHANGED
package/src/daemon.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const http = require('http');
|
|
2
2
|
const crypto = require('crypto');
|
|
3
|
-
const {
|
|
3
|
+
const { exec } = require('child_process');
|
|
4
4
|
const util = require('util');
|
|
5
5
|
const execAsync = util.promisify(exec);
|
|
6
6
|
const { Telegraf, Markup } = require('telegraf');
|
|
@@ -32,9 +32,12 @@ let config;
|
|
|
32
32
|
|
|
33
33
|
// --- Utilities ---
|
|
34
34
|
|
|
35
|
+
// Non-blocking log using a write stream
|
|
36
|
+
const logStream = fs.createWriteStream(LOG_PATH, { flags: 'a' });
|
|
37
|
+
logStream.on('error', () => {}); // prevent stream errors from crashing
|
|
38
|
+
|
|
35
39
|
function log(msg) {
|
|
36
|
-
|
|
37
|
-
fs.appendFileSync(LOG_PATH, line);
|
|
40
|
+
logStream.write(`[${new Date().toISOString()}] ${msg}\n`);
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
function projectLabel(cwd) {
|
|
@@ -76,14 +79,14 @@ function trackSession(data) {
|
|
|
76
79
|
|
|
77
80
|
/**
|
|
78
81
|
* Check if a TTY has any active processes (terminal is still open + something running).
|
|
79
|
-
*
|
|
82
|
+
* Non-blocking async version to avoid blocking the event loop.
|
|
80
83
|
*/
|
|
81
|
-
function isTtyAlive(ttyPath) {
|
|
84
|
+
async function isTtyAlive(ttyPath) {
|
|
82
85
|
if (!ttyPath) return false;
|
|
83
86
|
try {
|
|
84
87
|
const ttyName = ttyPath.replace('/dev/', '');
|
|
85
|
-
const
|
|
86
|
-
return
|
|
88
|
+
const { stdout } = await execAsync(`ps -t ${ttyName} -o pid= 2>/dev/null`, { timeout: 2000 });
|
|
89
|
+
return stdout.trim().length > 0;
|
|
87
90
|
} catch {
|
|
88
91
|
return false;
|
|
89
92
|
}
|
|
@@ -93,7 +96,12 @@ function isTtyAlive(ttyPath) {
|
|
|
93
96
|
* Escape a string for use inside AppleScript double-quoted strings.
|
|
94
97
|
*/
|
|
95
98
|
function escapeAppleScript(str) {
|
|
96
|
-
return str
|
|
99
|
+
return str
|
|
100
|
+
.replace(/\\/g, '\\\\')
|
|
101
|
+
.replace(/"/g, '\\"')
|
|
102
|
+
.replace(/\n/g, '\\n')
|
|
103
|
+
.replace(/\r/g, '\\r')
|
|
104
|
+
.replace(/\t/g, '\\t');
|
|
97
105
|
}
|
|
98
106
|
|
|
99
107
|
/**
|
|
@@ -450,9 +458,13 @@ async function sendElicitationQuestion(elicId) {
|
|
|
450
458
|
}
|
|
451
459
|
|
|
452
460
|
const keyboard = Markup.inlineKeyboard(rows);
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
461
|
+
try {
|
|
462
|
+
const sent = await bot.telegram.sendMessage(config.chatId, msg, keyboard);
|
|
463
|
+
elic.telegramMessageIds.push(sent.message_id);
|
|
464
|
+
elic.currentMessageId = sent.message_id;
|
|
465
|
+
} catch (err) {
|
|
466
|
+
log(`Elicitation question send failed: ${err.message}`);
|
|
467
|
+
}
|
|
456
468
|
}
|
|
457
469
|
|
|
458
470
|
/**
|
|
@@ -493,8 +505,12 @@ async function sendElicitationSummary(elicId) {
|
|
|
493
505
|
}
|
|
494
506
|
const keyboard = Markup.inlineKeyboard(summaryButtons);
|
|
495
507
|
|
|
496
|
-
|
|
497
|
-
|
|
508
|
+
try {
|
|
509
|
+
const sent = await bot.telegram.sendMessage(config.chatId, msg, keyboard);
|
|
510
|
+
elic.telegramMessageIds.push(sent.message_id);
|
|
511
|
+
} catch (err) {
|
|
512
|
+
log(`Elicitation summary send failed: ${err.message}`);
|
|
513
|
+
}
|
|
498
514
|
}
|
|
499
515
|
|
|
500
516
|
/**
|
|
@@ -591,14 +607,18 @@ async function handleElicitationCallback(ctx, cbData) {
|
|
|
591
607
|
try { await ctx.editMessageReplyMarkup(undefined); } catch {}
|
|
592
608
|
try { await ctx.editMessageText(ctx.callbackQuery.message.text + '\n\n✏️ Selected: Custom'); } catch {}
|
|
593
609
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
610
|
+
try {
|
|
611
|
+
const prompt = await bot.telegram.sendMessage(
|
|
612
|
+
config.chatId,
|
|
613
|
+
`✏️ Type your custom answer for: "${q.question}"\n\nReply to this message with your answer.`
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
elic.customWaitingMessageId = prompt.message_id;
|
|
617
|
+
elic.customWaitingQIdx = qIdx;
|
|
618
|
+
elic.telegramMessageIds.push(prompt.message_id);
|
|
619
|
+
} catch (err) {
|
|
620
|
+
log(`Elicitation custom prompt send failed: ${err.message}`);
|
|
621
|
+
}
|
|
602
622
|
log(`Elicitation ${elicId}: waiting for custom answer to q${qIdx}`);
|
|
603
623
|
return;
|
|
604
624
|
}
|
|
@@ -831,10 +851,15 @@ function startBot() {
|
|
|
831
851
|
|
|
832
852
|
bot.command('status', async (ctx) => {
|
|
833
853
|
const pendingCount = pendingQuestions.size;
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
854
|
+
const allSessions = [...sessions.entries()];
|
|
855
|
+
const activeSessions = [];
|
|
856
|
+
for (const [sid, s] of allSessions) {
|
|
857
|
+
if (s.ttyPath) {
|
|
858
|
+
if (await isTtyAlive(s.ttyPath)) activeSessions.push([sid, s]);
|
|
859
|
+
} else if (Date.now() - s.lastActive < 60 * 60 * 1000) {
|
|
860
|
+
activeSessions.push([sid, s]);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
838
863
|
|
|
839
864
|
let msg = '';
|
|
840
865
|
if (activeSessions.length === 0 && pendingCount === 0) {
|
|
@@ -909,6 +934,9 @@ function startBot() {
|
|
|
909
934
|
} else if (action === 'always') {
|
|
910
935
|
label = '✅ Always Allowed';
|
|
911
936
|
pending.resolve({ decision: 'always' });
|
|
937
|
+
} else {
|
|
938
|
+
label = '❌ Denied';
|
|
939
|
+
pending.resolve({ decision: 'deny' });
|
|
912
940
|
}
|
|
913
941
|
|
|
914
942
|
try { await ctx.answerCbQuery(label); } catch {}
|
|
@@ -983,14 +1011,16 @@ function startBot() {
|
|
|
983
1011
|
// If no reply-to, try auto-routing
|
|
984
1012
|
if (!targetSessionId) {
|
|
985
1013
|
// Find sessions that have notifications and are still alive (TTY exists)
|
|
986
|
-
const
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
})
|
|
993
|
-
|
|
1014
|
+
const allNotifs = [...messageToSession.entries()].filter(([, m]) => m.type === 'notification');
|
|
1015
|
+
const recentNotifications = [];
|
|
1016
|
+
for (const [, m] of allNotifs) {
|
|
1017
|
+
const s = sessions.get(m.sessionId);
|
|
1018
|
+
if (s?.ttyPath) {
|
|
1019
|
+
if (await isTtyAlive(s.ttyPath)) recentNotifications.push(m.sessionId);
|
|
1020
|
+
} else if (Date.now() - m.createdAt < 60 * 60 * 1000) {
|
|
1021
|
+
recentNotifications.push(m.sessionId);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
994
1024
|
|
|
995
1025
|
const uniqueSessions = [...new Set(recentNotifications)];
|
|
996
1026
|
|
|
@@ -1028,11 +1058,47 @@ function startBot() {
|
|
|
1028
1058
|
});
|
|
1029
1059
|
|
|
1030
1060
|
bot.catch((err) => {
|
|
1031
|
-
log(`Bot error: ${err
|
|
1061
|
+
log(`Bot error: ${err?.message || err}`);
|
|
1032
1062
|
});
|
|
1033
1063
|
|
|
1064
|
+
// Launch polling in background — don't block the HTTP server
|
|
1065
|
+
// Note: Telegraf's launch() promise never settles in v4.16 — that's OK,
|
|
1066
|
+
// polling starts immediately in the background.
|
|
1034
1067
|
bot.launch({ dropPendingUpdates: true });
|
|
1035
|
-
log('Telegram bot started');
|
|
1068
|
+
log('Telegram bot polling started');
|
|
1069
|
+
|
|
1070
|
+
// Send startup notification after brief delay (let polling initialize)
|
|
1071
|
+
setTimeout(async () => {
|
|
1072
|
+
try {
|
|
1073
|
+
await bot.telegram.sendMessage(config.chatId, '🟢 claude-tg daemon started');
|
|
1074
|
+
log('Startup notification sent to Telegram');
|
|
1075
|
+
} catch (e) {
|
|
1076
|
+
log(`Startup notification failed: ${e?.message || e}`);
|
|
1077
|
+
}
|
|
1078
|
+
}, 2000);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Periodic health check — verify Telegram connectivity.
|
|
1083
|
+
* If bot polling dies silently, restart it.
|
|
1084
|
+
*/
|
|
1085
|
+
let healthCheckRestarting = false;
|
|
1086
|
+
function startHealthCheck() {
|
|
1087
|
+
setInterval(async () => {
|
|
1088
|
+
if (healthCheckRestarting) return;
|
|
1089
|
+
try {
|
|
1090
|
+
await bot.telegram.getMe();
|
|
1091
|
+
} catch (err) {
|
|
1092
|
+
log(`Telegram health check failed: ${err?.message || err}. Restarting bot...`);
|
|
1093
|
+
healthCheckRestarting = true;
|
|
1094
|
+
try { bot.stop('health_restart'); } catch {}
|
|
1095
|
+
// Wait for old polling to fully stop before starting new instance
|
|
1096
|
+
setTimeout(() => {
|
|
1097
|
+
startBot();
|
|
1098
|
+
healthCheckRestarting = false;
|
|
1099
|
+
}, 3000);
|
|
1100
|
+
}
|
|
1101
|
+
}, 3 * 60 * 1000); // every 3 minutes
|
|
1036
1102
|
}
|
|
1037
1103
|
|
|
1038
1104
|
// --- HTTP handlers ---
|
|
@@ -1056,7 +1122,13 @@ async function sendPermissionRequest(data) {
|
|
|
1056
1122
|
Markup.button.callback('Always Allow', `${rid}:always`),
|
|
1057
1123
|
]);
|
|
1058
1124
|
|
|
1059
|
-
|
|
1125
|
+
let sent;
|
|
1126
|
+
try {
|
|
1127
|
+
sent = await bot.telegram.sendMessage(config.chatId, msg, keyboard);
|
|
1128
|
+
} catch (err) {
|
|
1129
|
+
log(`Permission send failed: ${err.message}`);
|
|
1130
|
+
return { decision: 'allow' }; // Fallback: allow if Telegram unreachable
|
|
1131
|
+
}
|
|
1060
1132
|
|
|
1061
1133
|
// Track this message for reply routing
|
|
1062
1134
|
messageToSession.set(sent.message_id, {
|
|
@@ -1245,14 +1317,14 @@ function startServer() {
|
|
|
1245
1317
|
// --- Cleanup stale state periodically ---
|
|
1246
1318
|
|
|
1247
1319
|
function startCleanup() {
|
|
1248
|
-
setInterval(() => {
|
|
1320
|
+
setInterval(async () => {
|
|
1249
1321
|
const now = Date.now();
|
|
1250
1322
|
const staleThreshold = 60 * 60 * 1000; // 1 hour
|
|
1251
1323
|
|
|
1252
1324
|
// Clean message mappings — keep if session's TTY is still alive
|
|
1253
1325
|
for (const [msgId, m] of messageToSession) {
|
|
1254
1326
|
const s = sessions.get(m.sessionId);
|
|
1255
|
-
if (s?.ttyPath && isTtyAlive(s.ttyPath)) continue;
|
|
1327
|
+
if (s?.ttyPath && await isTtyAlive(s.ttyPath)) continue;
|
|
1256
1328
|
if (now - m.createdAt > staleThreshold) {
|
|
1257
1329
|
messageToSession.delete(msgId);
|
|
1258
1330
|
}
|
|
@@ -1261,7 +1333,7 @@ function startCleanup() {
|
|
|
1261
1333
|
// Clean stale sessions — only if terminal is gone
|
|
1262
1334
|
for (const [sid, s] of sessions) {
|
|
1263
1335
|
if (s.ttyPath) {
|
|
1264
|
-
if (!isTtyAlive(s.ttyPath)) {
|
|
1336
|
+
if (!(await isTtyAlive(s.ttyPath))) {
|
|
1265
1337
|
sessions.delete(sid);
|
|
1266
1338
|
log(`Cleaned session ${sid} (TTY gone: ${s.ttyPath})`);
|
|
1267
1339
|
}
|
|
@@ -1291,9 +1363,10 @@ function main() {
|
|
|
1291
1363
|
}
|
|
1292
1364
|
|
|
1293
1365
|
log('Daemon starting...');
|
|
1294
|
-
|
|
1295
|
-
|
|
1366
|
+
startServer(); // HTTP server first — hooks must be able to connect immediately
|
|
1367
|
+
startBot(); // Telegram polling in background
|
|
1296
1368
|
startCleanup();
|
|
1369
|
+
startHealthCheck();
|
|
1297
1370
|
|
|
1298
1371
|
process.on('unhandledRejection', (err) => {
|
|
1299
1372
|
log(`Unhandled rejection: ${err?.message || err}`);
|