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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-tg",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Control Claude Code from Telegram — approve permissions, answer questions, reply to idle sessions, send files, all from your phone",
5
5
  "bin": {
6
6
  "claude-tg": "./bin/claude-tg.js"
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 escaped = escapeAppleScript(text.trim());
111
+ const trimmed = text.trim();
112
+ const escaped = escapeAppleScript(trimmed);
109
113
 
110
- // Try iTerm2 write text targets a specific session by TTY, no focus needed
114
+ // Check which terminal apps are running (fast, non-blocking check)
115
+ let itermRunning = false;
116
+ let terminalRunning = false;
111
117
  try {
112
- const script = `
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
- execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, { timeout: 5000, stdio: 'pipe' });
126
- log(`Sent via iTerm2 to ${ttyPath}: ${truncate(text, 80)}`);
127
- return true;
128
- } catch {}
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
- try {
132
- // Use clipboard paste instead of keystroke (reliable for any text length)
133
- const tmpTextFile = '/tmp/claude-tg-input.txt';
134
- fs.writeFileSync(tmpTextFile, text.trim());
135
-
136
- const script = [
137
- 'tell application "Terminal"',
138
- ' activate',
139
- ' repeat with w in windows',
140
- ' repeat with t in tabs of w',
141
- ` if tty of t is "${ttyPath}" then`,
142
- ' set selected tab of w to t',
143
- ' set index of w to 1',
144
- ' end if',
145
- ' end repeat',
146
- ' end repeat',
147
- 'end tell',
148
- `do shell script "cat ${tmpTextFile} | /usr/bin/pbcopy"`,
149
- 'delay 0.5',
150
- 'tell application "System Events"',
151
- ' tell process "Terminal"',
152
- ' keystroke "v" using command down',
153
- ' delay 0.3',
154
- ' key code 36',
155
- ' end tell',
156
- 'end tell',
157
- ].join('\n');
158
-
159
- fs.writeFileSync('/tmp/claude-tg-input.scpt', script);
160
- execSync('osascript /tmp/claude-tg-input.scpt', { timeout: 10000, stdio: 'pipe' });
161
- log(`Sent via Terminal.app to ${ttyPath}: ${truncate(text, 80)}`);
162
- return true;
163
- } catch (err) {
164
- log(`Terminal.app send error: ${err.message}`);
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 }); // type custom text
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: ' ' }); // space to toggle
726
+ events.push({ type: 'keystroke', value: ' ' });
703
727
  }
704
728
  if (i < q.options.length - 1) {
705
- events.push({ type: 'key_code', value: 125 }); // arrow down
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 }); // arrow down
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 (focus + System Events)
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
- execSync('osascript /tmp/claude-tg-elicit.scpt', { timeout: 30000, stdio: 'pipe' });
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 frontmost of w to true',
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
- execSync('osascript /tmp/claude-tg-elicit.scpt', { timeout: 30000, stdio: 'pipe' });
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 ok = sendInputToTerminal(session.ttyPath, text);
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(`⚠️ Could not send to terminal. Session may have ended, or terminal app not recognized.`); } catch {}
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
- const sent = await bot.telegram.sendMessage(config.chatId, msg);
1158
+ try {
1159
+ const sent = await bot.telegram.sendMessage(config.chatId, msg);
1138
1160
 
1139
- messageToSession.set(sent.message_id, {
1140
- sessionId: data.session_id,
1141
- type: 'notification',
1142
- createdAt: Date.now(),
1143
- });
1161
+ messageToSession.set(sent.message_id, {
1162
+ sessionId: data.session_id,
1163
+ type: 'notification',
1164
+ createdAt: Date.now(),
1165
+ });
1144
1166
 
1145
- log(`Notification sent: ${notifType} (msg ${sent.message_id})`);
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) {
@@ -6,7 +6,7 @@
6
6
  const http = require('http');
7
7
  const { execSync } = require('child_process');
8
8
 
9
- const DAEMON_PORT = 7483;
9
+ const DAEMON_PORT = parseInt(process.env.CLAUDE_TG_PORT || '7483', 10);
10
10
  const DAEMON_HOST = '127.0.0.1';
11
11
 
12
12
  function readStdin() {
@@ -7,7 +7,7 @@
7
7
  const http = require('http');
8
8
  const { execSync } = require('child_process');
9
9
 
10
- const DAEMON_PORT = 7483;
10
+ const DAEMON_PORT = parseInt(process.env.CLAUDE_TG_PORT || '7483', 10);
11
11
  const DAEMON_HOST = '127.0.0.1';
12
12
 
13
13
  function findTty() {
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: `node ${path.join(HOOKS_DIR, 'permission-request.js')}`,
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: `node ${path.join(HOOKS_DIR, 'notification.js')}`,
48
+ command: `${envPrefix}node ${path.join(HOOKS_DIR, 'notification.js')}`,
48
49
  async: true,
49
50
  },
50
51
  ],