claude-tg 1.0.5 → 1.0.6
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/package.json +1 -1
- package/src/daemon.js +99 -74
- package/src/hooks/notification.js +1 -1
- package/src/hooks/permission-request.js +1 -1
- package/src/setup.js +3 -2
package/package.json
CHANGED
package/src/daemon.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const http = require('http');
|
|
2
2
|
const crypto = require('crypto');
|
|
3
|
-
const { execSync } = require('child_process');
|
|
3
|
+
const { execSync, exec } = require('child_process');
|
|
4
|
+
const util = require('util');
|
|
5
|
+
const execAsync = util.promisify(exec);
|
|
4
6
|
const { Telegraf, Markup } = require('telegraf');
|
|
5
7
|
const { loadConfig, LOG_PATH } = require('./config');
|
|
6
8
|
const fs = require('fs');
|
|
@@ -97,19 +99,35 @@ function escapeAppleScript(str) {
|
|
|
97
99
|
/**
|
|
98
100
|
* Send text as input to a terminal session identified by its TTY path.
|
|
99
101
|
* Uses osascript to type into the correct terminal tab/session.
|
|
100
|
-
* Tries iTerm2 first, then Terminal.app.
|
|
102
|
+
* Tries iTerm2 first, then Terminal.app. Non-blocking (async).
|
|
103
|
+
* Returns { ok: true } on success, { ok: false, error: string } on failure.
|
|
101
104
|
*/
|
|
102
|
-
function sendInputToTerminal(ttyPath, text) {
|
|
105
|
+
async function sendInputToTerminal(ttyPath, text) {
|
|
103
106
|
if (!ttyPath) {
|
|
104
107
|
log('sendInput: no TTY path');
|
|
105
|
-
return false;
|
|
108
|
+
return { ok: false, error: 'No TTY path for this session' };
|
|
106
109
|
}
|
|
107
110
|
|
|
108
|
-
const
|
|
111
|
+
const trimmed = text.trim();
|
|
112
|
+
const escaped = escapeAppleScript(trimmed);
|
|
109
113
|
|
|
110
|
-
//
|
|
114
|
+
// Check which terminal apps are running (fast, non-blocking check)
|
|
115
|
+
let itermRunning = false;
|
|
116
|
+
let terminalRunning = false;
|
|
111
117
|
try {
|
|
112
|
-
const
|
|
118
|
+
const { stdout } = await execAsync('ps -c -o comm= | grep -E "^(iTerm2|Terminal)$"', { timeout: 2000 });
|
|
119
|
+
itermRunning = stdout.includes('iTerm2');
|
|
120
|
+
terminalRunning = stdout.includes('Terminal');
|
|
121
|
+
} catch {
|
|
122
|
+
// ps/grep failed, try both
|
|
123
|
+
itermRunning = true;
|
|
124
|
+
terminalRunning = true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Try iTerm2 — write text targets a specific session by TTY, no focus needed
|
|
128
|
+
if (itermRunning) {
|
|
129
|
+
try {
|
|
130
|
+
const script = `
|
|
113
131
|
tell application "iTerm2"
|
|
114
132
|
repeat with w in windows
|
|
115
133
|
repeat with t in tabs of w
|
|
@@ -122,50 +140,58 @@ tell application "iTerm2"
|
|
|
122
140
|
end repeat
|
|
123
141
|
end repeat
|
|
124
142
|
end tell`;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
143
|
+
await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, { timeout: 5000 });
|
|
144
|
+
log(`Sent via iTerm2 to ${ttyPath}: ${truncate(text, 80)}`);
|
|
145
|
+
return { ok: true };
|
|
146
|
+
} catch (err) {
|
|
147
|
+
log(`iTerm2 attempt: ${(err.message || '').slice(0, 100)}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
129
150
|
|
|
130
151
|
// Try Terminal.app — focus the tab, paste text from clipboard, press Enter
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
152
|
+
if (terminalRunning) {
|
|
153
|
+
try {
|
|
154
|
+
const tmpTextFile = '/tmp/claude-tg-input.txt';
|
|
155
|
+
fs.writeFileSync(tmpTextFile, trimmed);
|
|
156
|
+
|
|
157
|
+
const script = [
|
|
158
|
+
'tell application "Terminal"',
|
|
159
|
+
' activate',
|
|
160
|
+
' repeat with w in windows',
|
|
161
|
+
' repeat with t in tabs of w',
|
|
162
|
+
` if tty of t is "${ttyPath}" then`,
|
|
163
|
+
' set selected tab of w to t',
|
|
164
|
+
' set index of w to 1',
|
|
165
|
+
' end if',
|
|
166
|
+
' end repeat',
|
|
167
|
+
' end repeat',
|
|
168
|
+
'end tell',
|
|
169
|
+
`do shell script "cat ${tmpTextFile} | /usr/bin/pbcopy"`,
|
|
170
|
+
'delay 0.5',
|
|
171
|
+
'tell application "System Events"',
|
|
172
|
+
' tell process "Terminal"',
|
|
173
|
+
' keystroke "v" using command down',
|
|
174
|
+
' delay 0.3',
|
|
175
|
+
' key code 36',
|
|
176
|
+
' end tell',
|
|
177
|
+
'end tell',
|
|
178
|
+
].join('\n');
|
|
179
|
+
|
|
180
|
+
fs.writeFileSync('/tmp/claude-tg-input.scpt', script);
|
|
181
|
+
await execAsync('osascript /tmp/claude-tg-input.scpt', { timeout: 15000 });
|
|
182
|
+
log(`Sent via Terminal.app to ${ttyPath}: ${truncate(text, 80)}`);
|
|
183
|
+
return { ok: true };
|
|
184
|
+
} catch (err) {
|
|
185
|
+
const msg = err.message || err.stderr || '';
|
|
186
|
+
log(`Terminal.app send error: ${msg.slice(0, 200)}`);
|
|
187
|
+
if (msg.includes('assistive') || msg.includes('accessibility') || msg.includes('not allowed')) {
|
|
188
|
+
return { ok: false, error: 'Accessibility permission needed.\nSystem Settings → Privacy & Security → Accessibility → enable Terminal/iTerm2.' };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
165
191
|
}
|
|
166
192
|
|
|
167
193
|
log(`sendInput failed: no terminal found for ${ttyPath}`);
|
|
168
|
-
return false;
|
|
194
|
+
return { ok: false, error: `Could not send to terminal (${ttyPath}). Make sure iTerm2 or Terminal.app is open.` };
|
|
169
195
|
}
|
|
170
196
|
|
|
171
197
|
// --- Transcript reading ---
|
|
@@ -503,8 +529,8 @@ async function handleElicitationCallback(ctx, cbData) {
|
|
|
503
529
|
elic.permissionResolve({ decision: 'allow' });
|
|
504
530
|
try { await ctx.editMessageText(ctx.callbackQuery.message.text + '\n\n✅ Answers submitted — injecting...'); } catch {}
|
|
505
531
|
|
|
506
|
-
setTimeout(() => {
|
|
507
|
-
const ok = injectElicitationAnswers(elic.ttyPath, elic.questions, elic.answers);
|
|
532
|
+
setTimeout(async () => {
|
|
533
|
+
const ok = await injectElicitationAnswers(elic.ttyPath, elic.questions, elic.answers);
|
|
508
534
|
log(`Elicitation ${elicId}: ${ok ? 'keystrokes injected' : 'injection failed'}`);
|
|
509
535
|
if (!ok) {
|
|
510
536
|
bot.telegram.sendMessage(config.chatId, '⚠️ Could not inject answers into terminal').catch(() => {});
|
|
@@ -512,7 +538,7 @@ async function handleElicitationCallback(ctx, cbData) {
|
|
|
512
538
|
}, 2000);
|
|
513
539
|
} else {
|
|
514
540
|
// From notification flow — inject immediately
|
|
515
|
-
const ok = injectElicitationAnswers(elic.ttyPath, elic.questions, elic.answers);
|
|
541
|
+
const ok = await injectElicitationAnswers(elic.ttyPath, elic.questions, elic.answers);
|
|
516
542
|
try {
|
|
517
543
|
await ctx.editMessageText(ctx.callbackQuery.message.text +
|
|
518
544
|
(ok ? '\n\n✅ Answers submitted' : '\n\n⚠️ Could not send to terminal'));
|
|
@@ -671,7 +697,7 @@ async function handleElicitationCallback(ctx, cbData) {
|
|
|
671
697
|
* Inject elicitation answers into the terminal via osascript keystrokes.
|
|
672
698
|
* Navigates the AskUserQuestion form using arrow keys, space, tab, and enter.
|
|
673
699
|
*/
|
|
674
|
-
function injectElicitationAnswers(ttyPath, questions, answers) {
|
|
700
|
+
async function injectElicitationAnswers(ttyPath, questions, answers) {
|
|
675
701
|
if (!ttyPath) {
|
|
676
702
|
log('injectElicitation: no TTY path');
|
|
677
703
|
return false;
|
|
@@ -686,44 +712,38 @@ function injectElicitationAnswers(ttyPath, questions, answers) {
|
|
|
686
712
|
if (!answer) continue;
|
|
687
713
|
|
|
688
714
|
if (answer.isCustom) {
|
|
689
|
-
// Navigate to "Other" (last position, after all defined options)
|
|
690
715
|
const otherPos = q.options.length;
|
|
691
716
|
for (let i = 0; i < otherPos; i++) {
|
|
692
717
|
events.push({ type: 'key_code', value: 125 }); // arrow down
|
|
693
718
|
}
|
|
694
719
|
events.push({ type: 'key_code', value: 36 }); // enter to select Other
|
|
695
720
|
events.push({ type: 'delay', value: 0.3 });
|
|
696
|
-
events.push({ type: 'keystroke', value: answer.customText });
|
|
721
|
+
events.push({ type: 'keystroke', value: answer.customText });
|
|
697
722
|
} else if (answer.multiSelections) {
|
|
698
|
-
// MultiSelect: walk through all options, space on selected ones
|
|
699
723
|
const selectedSet = new Set(answer.multiSelections.map((s) => s.optionIndex));
|
|
700
724
|
for (let i = 0; i < q.options.length; i++) {
|
|
701
725
|
if (selectedSet.has(i)) {
|
|
702
|
-
events.push({ type: 'keystroke', value: ' ' });
|
|
726
|
+
events.push({ type: 'keystroke', value: ' ' });
|
|
703
727
|
}
|
|
704
728
|
if (i < q.options.length - 1) {
|
|
705
|
-
events.push({ type: 'key_code', value: 125 });
|
|
729
|
+
events.push({ type: 'key_code', value: 125 });
|
|
706
730
|
}
|
|
707
731
|
}
|
|
708
732
|
} else {
|
|
709
|
-
// Single select: navigate to selected option
|
|
710
733
|
for (let i = 0; i < answer.optionIndex; i++) {
|
|
711
|
-
events.push({ type: 'key_code', value: 125 });
|
|
734
|
+
events.push({ type: 'key_code', value: 125 });
|
|
712
735
|
}
|
|
713
736
|
}
|
|
714
737
|
|
|
715
|
-
// Tab to next question, or nothing for the last one
|
|
716
738
|
if (qIdx < questions.length - 1) {
|
|
717
739
|
events.push({ type: 'key_code', value: 48 }); // tab
|
|
718
740
|
events.push({ type: 'delay', value: 0.15 });
|
|
719
741
|
}
|
|
720
742
|
}
|
|
721
743
|
|
|
722
|
-
// Submit the form
|
|
723
744
|
events.push({ type: 'delay', value: 0.3 });
|
|
724
745
|
events.push({ type: 'key_code', value: 36 }); // Return key
|
|
725
746
|
|
|
726
|
-
// Build key action lines for AppleScript
|
|
727
747
|
const keyLines = events.map((e) => {
|
|
728
748
|
if (e.type === 'key_code') return ` key code ${e.value}`;
|
|
729
749
|
if (e.type === 'keystroke') {
|
|
@@ -735,7 +755,7 @@ function injectElicitationAnswers(ttyPath, questions, answers) {
|
|
|
735
755
|
return '';
|
|
736
756
|
}).join('\n');
|
|
737
757
|
|
|
738
|
-
// Try iTerm2 first
|
|
758
|
+
// Try iTerm2 first
|
|
739
759
|
try {
|
|
740
760
|
const script = [
|
|
741
761
|
'tell application "iTerm2"',
|
|
@@ -759,7 +779,7 @@ function injectElicitationAnswers(ttyPath, questions, answers) {
|
|
|
759
779
|
].join('\n');
|
|
760
780
|
|
|
761
781
|
fs.writeFileSync('/tmp/claude-tg-elicit.scpt', script);
|
|
762
|
-
|
|
782
|
+
await execAsync('osascript /tmp/claude-tg-elicit.scpt', { timeout: 30000 });
|
|
763
783
|
log(`Elicitation keystrokes sent via iTerm2 to ${ttyPath}`);
|
|
764
784
|
return true;
|
|
765
785
|
} catch {}
|
|
@@ -768,11 +788,12 @@ function injectElicitationAnswers(ttyPath, questions, answers) {
|
|
|
768
788
|
try {
|
|
769
789
|
const script = [
|
|
770
790
|
'tell application "Terminal"',
|
|
791
|
+
' activate',
|
|
771
792
|
' repeat with w in windows',
|
|
772
793
|
' repeat with t in tabs of w',
|
|
773
794
|
` if tty of t is "${ttyPath}" then`,
|
|
774
795
|
' set selected tab of w to t',
|
|
775
|
-
' set
|
|
796
|
+
' set index of w to 1',
|
|
776
797
|
' end if',
|
|
777
798
|
' end repeat',
|
|
778
799
|
' end repeat',
|
|
@@ -786,7 +807,7 @@ function injectElicitationAnswers(ttyPath, questions, answers) {
|
|
|
786
807
|
].join('\n');
|
|
787
808
|
|
|
788
809
|
fs.writeFileSync('/tmp/claude-tg-elicit.scpt', script);
|
|
789
|
-
|
|
810
|
+
await execAsync('osascript /tmp/claude-tg-elicit.scpt', { timeout: 30000 });
|
|
790
811
|
log(`Elicitation keystrokes sent via Terminal.app to ${ttyPath}`);
|
|
791
812
|
return true;
|
|
792
813
|
} catch (err) {
|
|
@@ -997,12 +1018,12 @@ function startBot() {
|
|
|
997
1018
|
return;
|
|
998
1019
|
}
|
|
999
1020
|
|
|
1000
|
-
const
|
|
1001
|
-
if (ok) {
|
|
1021
|
+
const result = await sendInputToTerminal(session.ttyPath, text);
|
|
1022
|
+
if (result.ok) {
|
|
1002
1023
|
const num = getSessionLabel(targetSessionId);
|
|
1003
1024
|
try { await ctx.reply(`➡️ Sent to #${num} ${session.label}`); } catch {}
|
|
1004
1025
|
} else {
|
|
1005
|
-
try { await ctx.reply(`⚠️
|
|
1026
|
+
try { await ctx.reply(`⚠️ ${result.error}`); } catch {}
|
|
1006
1027
|
}
|
|
1007
1028
|
});
|
|
1008
1029
|
|
|
@@ -1134,15 +1155,19 @@ async function sendNotification(data) {
|
|
|
1134
1155
|
// Default notification flow
|
|
1135
1156
|
const context = extractContext(data.transcript_path);
|
|
1136
1157
|
const msg = formatNotification(data, context);
|
|
1137
|
-
|
|
1158
|
+
try {
|
|
1159
|
+
const sent = await bot.telegram.sendMessage(config.chatId, msg);
|
|
1138
1160
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1161
|
+
messageToSession.set(sent.message_id, {
|
|
1162
|
+
sessionId: data.session_id,
|
|
1163
|
+
type: 'notification',
|
|
1164
|
+
createdAt: Date.now(),
|
|
1165
|
+
});
|
|
1144
1166
|
|
|
1145
|
-
|
|
1167
|
+
log(`Notification sent: ${notifType} (msg ${sent.message_id})`);
|
|
1168
|
+
} catch (err) {
|
|
1169
|
+
log(`Notification send failed: ${err.message}`);
|
|
1170
|
+
}
|
|
1146
1171
|
}
|
|
1147
1172
|
|
|
1148
1173
|
function readBody(req) {
|
package/src/setup.js
CHANGED
|
@@ -25,13 +25,14 @@ function telegramApiCall(token, method) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function getHooksConfig(port) {
|
|
28
|
+
const envPrefix = port !== 7483 ? `CLAUDE_TG_PORT=${port} ` : '';
|
|
28
29
|
return {
|
|
29
30
|
PermissionRequest: [
|
|
30
31
|
{
|
|
31
32
|
hooks: [
|
|
32
33
|
{
|
|
33
34
|
type: 'command',
|
|
34
|
-
command:
|
|
35
|
+
command: `${envPrefix}node ${path.join(HOOKS_DIR, 'permission-request.js')}`,
|
|
35
36
|
timeout: 1800,
|
|
36
37
|
statusMessage: 'Waiting for Telegram approval...',
|
|
37
38
|
},
|
|
@@ -44,7 +45,7 @@ function getHooksConfig(port) {
|
|
|
44
45
|
hooks: [
|
|
45
46
|
{
|
|
46
47
|
type: 'command',
|
|
47
|
-
command:
|
|
48
|
+
command: `${envPrefix}node ${path.join(HOOKS_DIR, 'notification.js')}`,
|
|
48
49
|
async: true,
|
|
49
50
|
},
|
|
50
51
|
],
|