claude-tg 1.0.5 → 1.0.7

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 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.replace(/^node\s+/, '');
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.replace(/^node\s+/, '');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-tg",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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');
@@ -91,25 +93,46 @@ function isTtyAlive(ttyPath) {
91
93
  * Escape a string for use inside AppleScript double-quoted strings.
92
94
  */
93
95
  function escapeAppleScript(str) {
94
- return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
96
+ return str
97
+ .replace(/\\/g, '\\\\')
98
+ .replace(/"/g, '\\"')
99
+ .replace(/\n/g, '\\n')
100
+ .replace(/\r/g, '\\r')
101
+ .replace(/\t/g, '\\t');
95
102
  }
96
103
 
97
104
  /**
98
105
  * Send text as input to a terminal session identified by its TTY path.
99
106
  * Uses osascript to type into the correct terminal tab/session.
100
- * Tries iTerm2 first, then Terminal.app.
107
+ * Tries iTerm2 first, then Terminal.app. Non-blocking (async).
108
+ * Returns { ok: true } on success, { ok: false, error: string } on failure.
101
109
  */
102
- function sendInputToTerminal(ttyPath, text) {
110
+ async function sendInputToTerminal(ttyPath, text) {
103
111
  if (!ttyPath) {
104
112
  log('sendInput: no TTY path');
105
- return false;
113
+ return { ok: false, error: 'No TTY path for this session' };
106
114
  }
107
115
 
108
- const escaped = escapeAppleScript(text.trim());
116
+ const trimmed = text.trim();
117
+ const escaped = escapeAppleScript(trimmed);
109
118
 
110
- // Try iTerm2 write text targets a specific session by TTY, no focus needed
119
+ // Check which terminal apps are running (fast, non-blocking check)
120
+ let itermRunning = false;
121
+ let terminalRunning = false;
111
122
  try {
112
- const script = `
123
+ const { stdout } = await execAsync('ps -c -o comm= | grep -E "^(iTerm2|Terminal)$"', { timeout: 2000 });
124
+ itermRunning = stdout.includes('iTerm2');
125
+ terminalRunning = stdout.includes('Terminal');
126
+ } catch {
127
+ // ps/grep failed, try both
128
+ itermRunning = true;
129
+ terminalRunning = true;
130
+ }
131
+
132
+ // Try iTerm2 — write text targets a specific session by TTY, no focus needed
133
+ if (itermRunning) {
134
+ try {
135
+ const script = `
113
136
  tell application "iTerm2"
114
137
  repeat with w in windows
115
138
  repeat with t in tabs of w
@@ -122,50 +145,58 @@ tell application "iTerm2"
122
145
  end repeat
123
146
  end repeat
124
147
  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 {}
148
+ await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, { timeout: 5000 });
149
+ log(`Sent via iTerm2 to ${ttyPath}: ${truncate(text, 80)}`);
150
+ return { ok: true };
151
+ } catch (err) {
152
+ log(`iTerm2 attempt: ${(err.message || '').slice(0, 100)}`);
153
+ }
154
+ }
129
155
 
130
156
  // 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}`);
157
+ if (terminalRunning) {
158
+ try {
159
+ const tmpTextFile = '/tmp/claude-tg-input.txt';
160
+ fs.writeFileSync(tmpTextFile, trimmed);
161
+
162
+ const script = [
163
+ 'tell application "Terminal"',
164
+ ' activate',
165
+ ' repeat with w in windows',
166
+ ' repeat with t in tabs of w',
167
+ ` if tty of t is "${ttyPath}" then`,
168
+ ' set selected tab of w to t',
169
+ ' set index of w to 1',
170
+ ' end if',
171
+ ' end repeat',
172
+ ' end repeat',
173
+ 'end tell',
174
+ `do shell script "cat ${tmpTextFile} | /usr/bin/pbcopy"`,
175
+ 'delay 0.5',
176
+ 'tell application "System Events"',
177
+ ' tell process "Terminal"',
178
+ ' keystroke "v" using command down',
179
+ ' delay 0.3',
180
+ ' key code 36',
181
+ ' end tell',
182
+ 'end tell',
183
+ ].join('\n');
184
+
185
+ fs.writeFileSync('/tmp/claude-tg-input.scpt', script);
186
+ await execAsync('osascript /tmp/claude-tg-input.scpt', { timeout: 15000 });
187
+ log(`Sent via Terminal.app to ${ttyPath}: ${truncate(text, 80)}`);
188
+ return { ok: true };
189
+ } catch (err) {
190
+ const msg = err.message || err.stderr || '';
191
+ log(`Terminal.app send error: ${msg.slice(0, 200)}`);
192
+ if (msg.includes('assistive') || msg.includes('accessibility') || msg.includes('not allowed')) {
193
+ return { ok: false, error: 'Accessibility permission needed.\nSystem Settings → Privacy & Security → Accessibility → enable Terminal/iTerm2.' };
194
+ }
195
+ }
165
196
  }
166
197
 
167
198
  log(`sendInput failed: no terminal found for ${ttyPath}`);
168
- return false;
199
+ return { ok: false, error: `Could not send to terminal (${ttyPath}). Make sure iTerm2 or Terminal.app is open.` };
169
200
  }
170
201
 
171
202
  // --- Transcript reading ---
@@ -424,9 +455,13 @@ async function sendElicitationQuestion(elicId) {
424
455
  }
425
456
 
426
457
  const keyboard = Markup.inlineKeyboard(rows);
427
- const sent = await bot.telegram.sendMessage(config.chatId, msg, keyboard);
428
- elic.telegramMessageIds.push(sent.message_id);
429
- elic.currentMessageId = sent.message_id;
458
+ try {
459
+ const sent = await bot.telegram.sendMessage(config.chatId, msg, keyboard);
460
+ elic.telegramMessageIds.push(sent.message_id);
461
+ elic.currentMessageId = sent.message_id;
462
+ } catch (err) {
463
+ log(`Elicitation question send failed: ${err.message}`);
464
+ }
430
465
  }
431
466
 
432
467
  /**
@@ -467,8 +502,12 @@ async function sendElicitationSummary(elicId) {
467
502
  }
468
503
  const keyboard = Markup.inlineKeyboard(summaryButtons);
469
504
 
470
- const sent = await bot.telegram.sendMessage(config.chatId, msg, keyboard);
471
- elic.telegramMessageIds.push(sent.message_id);
505
+ try {
506
+ const sent = await bot.telegram.sendMessage(config.chatId, msg, keyboard);
507
+ elic.telegramMessageIds.push(sent.message_id);
508
+ } catch (err) {
509
+ log(`Elicitation summary send failed: ${err.message}`);
510
+ }
472
511
  }
473
512
 
474
513
  /**
@@ -503,8 +542,8 @@ async function handleElicitationCallback(ctx, cbData) {
503
542
  elic.permissionResolve({ decision: 'allow' });
504
543
  try { await ctx.editMessageText(ctx.callbackQuery.message.text + '\n\n✅ Answers submitted — injecting...'); } catch {}
505
544
 
506
- setTimeout(() => {
507
- const ok = injectElicitationAnswers(elic.ttyPath, elic.questions, elic.answers);
545
+ setTimeout(async () => {
546
+ const ok = await injectElicitationAnswers(elic.ttyPath, elic.questions, elic.answers);
508
547
  log(`Elicitation ${elicId}: ${ok ? 'keystrokes injected' : 'injection failed'}`);
509
548
  if (!ok) {
510
549
  bot.telegram.sendMessage(config.chatId, '⚠️ Could not inject answers into terminal').catch(() => {});
@@ -512,7 +551,7 @@ async function handleElicitationCallback(ctx, cbData) {
512
551
  }, 2000);
513
552
  } else {
514
553
  // From notification flow — inject immediately
515
- const ok = injectElicitationAnswers(elic.ttyPath, elic.questions, elic.answers);
554
+ const ok = await injectElicitationAnswers(elic.ttyPath, elic.questions, elic.answers);
516
555
  try {
517
556
  await ctx.editMessageText(ctx.callbackQuery.message.text +
518
557
  (ok ? '\n\n✅ Answers submitted' : '\n\n⚠️ Could not send to terminal'));
@@ -565,14 +604,18 @@ async function handleElicitationCallback(ctx, cbData) {
565
604
  try { await ctx.editMessageReplyMarkup(undefined); } catch {}
566
605
  try { await ctx.editMessageText(ctx.callbackQuery.message.text + '\n\n✏️ Selected: Custom'); } catch {}
567
606
 
568
- const prompt = await bot.telegram.sendMessage(
569
- config.chatId,
570
- `✏️ Type your custom answer for: "${q.question}"\n\nReply to this message with your answer.`
571
- );
572
-
573
- elic.customWaitingMessageId = prompt.message_id;
574
- elic.customWaitingQIdx = qIdx;
575
- elic.telegramMessageIds.push(prompt.message_id);
607
+ try {
608
+ const prompt = await bot.telegram.sendMessage(
609
+ config.chatId,
610
+ `✏️ Type your custom answer for: "${q.question}"\n\nReply to this message with your answer.`
611
+ );
612
+
613
+ elic.customWaitingMessageId = prompt.message_id;
614
+ elic.customWaitingQIdx = qIdx;
615
+ elic.telegramMessageIds.push(prompt.message_id);
616
+ } catch (err) {
617
+ log(`Elicitation custom prompt send failed: ${err.message}`);
618
+ }
576
619
  log(`Elicitation ${elicId}: waiting for custom answer to q${qIdx}`);
577
620
  return;
578
621
  }
@@ -671,7 +714,7 @@ async function handleElicitationCallback(ctx, cbData) {
671
714
  * Inject elicitation answers into the terminal via osascript keystrokes.
672
715
  * Navigates the AskUserQuestion form using arrow keys, space, tab, and enter.
673
716
  */
674
- function injectElicitationAnswers(ttyPath, questions, answers) {
717
+ async function injectElicitationAnswers(ttyPath, questions, answers) {
675
718
  if (!ttyPath) {
676
719
  log('injectElicitation: no TTY path');
677
720
  return false;
@@ -686,44 +729,38 @@ function injectElicitationAnswers(ttyPath, questions, answers) {
686
729
  if (!answer) continue;
687
730
 
688
731
  if (answer.isCustom) {
689
- // Navigate to "Other" (last position, after all defined options)
690
732
  const otherPos = q.options.length;
691
733
  for (let i = 0; i < otherPos; i++) {
692
734
  events.push({ type: 'key_code', value: 125 }); // arrow down
693
735
  }
694
736
  events.push({ type: 'key_code', value: 36 }); // enter to select Other
695
737
  events.push({ type: 'delay', value: 0.3 });
696
- events.push({ type: 'keystroke', value: answer.customText }); // type custom text
738
+ events.push({ type: 'keystroke', value: answer.customText });
697
739
  } else if (answer.multiSelections) {
698
- // MultiSelect: walk through all options, space on selected ones
699
740
  const selectedSet = new Set(answer.multiSelections.map((s) => s.optionIndex));
700
741
  for (let i = 0; i < q.options.length; i++) {
701
742
  if (selectedSet.has(i)) {
702
- events.push({ type: 'keystroke', value: ' ' }); // space to toggle
743
+ events.push({ type: 'keystroke', value: ' ' });
703
744
  }
704
745
  if (i < q.options.length - 1) {
705
- events.push({ type: 'key_code', value: 125 }); // arrow down
746
+ events.push({ type: 'key_code', value: 125 });
706
747
  }
707
748
  }
708
749
  } else {
709
- // Single select: navigate to selected option
710
750
  for (let i = 0; i < answer.optionIndex; i++) {
711
- events.push({ type: 'key_code', value: 125 }); // arrow down
751
+ events.push({ type: 'key_code', value: 125 });
712
752
  }
713
753
  }
714
754
 
715
- // Tab to next question, or nothing for the last one
716
755
  if (qIdx < questions.length - 1) {
717
756
  events.push({ type: 'key_code', value: 48 }); // tab
718
757
  events.push({ type: 'delay', value: 0.15 });
719
758
  }
720
759
  }
721
760
 
722
- // Submit the form
723
761
  events.push({ type: 'delay', value: 0.3 });
724
762
  events.push({ type: 'key_code', value: 36 }); // Return key
725
763
 
726
- // Build key action lines for AppleScript
727
764
  const keyLines = events.map((e) => {
728
765
  if (e.type === 'key_code') return ` key code ${e.value}`;
729
766
  if (e.type === 'keystroke') {
@@ -735,7 +772,7 @@ function injectElicitationAnswers(ttyPath, questions, answers) {
735
772
  return '';
736
773
  }).join('\n');
737
774
 
738
- // Try iTerm2 first (focus + System Events)
775
+ // Try iTerm2 first
739
776
  try {
740
777
  const script = [
741
778
  'tell application "iTerm2"',
@@ -759,7 +796,7 @@ function injectElicitationAnswers(ttyPath, questions, answers) {
759
796
  ].join('\n');
760
797
 
761
798
  fs.writeFileSync('/tmp/claude-tg-elicit.scpt', script);
762
- execSync('osascript /tmp/claude-tg-elicit.scpt', { timeout: 30000, stdio: 'pipe' });
799
+ await execAsync('osascript /tmp/claude-tg-elicit.scpt', { timeout: 30000 });
763
800
  log(`Elicitation keystrokes sent via iTerm2 to ${ttyPath}`);
764
801
  return true;
765
802
  } catch {}
@@ -768,11 +805,12 @@ function injectElicitationAnswers(ttyPath, questions, answers) {
768
805
  try {
769
806
  const script = [
770
807
  'tell application "Terminal"',
808
+ ' activate',
771
809
  ' repeat with w in windows',
772
810
  ' repeat with t in tabs of w',
773
811
  ` if tty of t is "${ttyPath}" then`,
774
812
  ' set selected tab of w to t',
775
- ' set frontmost of w to true',
813
+ ' set index of w to 1',
776
814
  ' end if',
777
815
  ' end repeat',
778
816
  ' end repeat',
@@ -786,7 +824,7 @@ function injectElicitationAnswers(ttyPath, questions, answers) {
786
824
  ].join('\n');
787
825
 
788
826
  fs.writeFileSync('/tmp/claude-tg-elicit.scpt', script);
789
- execSync('osascript /tmp/claude-tg-elicit.scpt', { timeout: 30000, stdio: 'pipe' });
827
+ await execAsync('osascript /tmp/claude-tg-elicit.scpt', { timeout: 30000 });
790
828
  log(`Elicitation keystrokes sent via Terminal.app to ${ttyPath}`);
791
829
  return true;
792
830
  } catch (err) {
@@ -888,6 +926,9 @@ function startBot() {
888
926
  } else if (action === 'always') {
889
927
  label = '✅ Always Allowed';
890
928
  pending.resolve({ decision: 'always' });
929
+ } else {
930
+ label = '❌ Denied';
931
+ pending.resolve({ decision: 'deny' });
891
932
  }
892
933
 
893
934
  try { await ctx.answerCbQuery(label); } catch {}
@@ -997,12 +1038,12 @@ function startBot() {
997
1038
  return;
998
1039
  }
999
1040
 
1000
- const ok = sendInputToTerminal(session.ttyPath, text);
1001
- if (ok) {
1041
+ const result = await sendInputToTerminal(session.ttyPath, text);
1042
+ if (result.ok) {
1002
1043
  const num = getSessionLabel(targetSessionId);
1003
1044
  try { await ctx.reply(`➡️ Sent to #${num} ${session.label}`); } catch {}
1004
1045
  } else {
1005
- try { await ctx.reply(`⚠️ Could not send to terminal. Session may have ended, or terminal app not recognized.`); } catch {}
1046
+ try { await ctx.reply(`⚠️ ${result.error}`); } catch {}
1006
1047
  }
1007
1048
  });
1008
1049
 
@@ -1035,7 +1076,13 @@ async function sendPermissionRequest(data) {
1035
1076
  Markup.button.callback('Always Allow', `${rid}:always`),
1036
1077
  ]);
1037
1078
 
1038
- const sent = await bot.telegram.sendMessage(config.chatId, msg, keyboard);
1079
+ let sent;
1080
+ try {
1081
+ sent = await bot.telegram.sendMessage(config.chatId, msg, keyboard);
1082
+ } catch (err) {
1083
+ log(`Permission send failed: ${err.message}`);
1084
+ return { decision: 'allow' }; // Fallback: allow if Telegram unreachable
1085
+ }
1039
1086
 
1040
1087
  // Track this message for reply routing
1041
1088
  messageToSession.set(sent.message_id, {
@@ -1134,15 +1181,19 @@ async function sendNotification(data) {
1134
1181
  // Default notification flow
1135
1182
  const context = extractContext(data.transcript_path);
1136
1183
  const msg = formatNotification(data, context);
1137
- const sent = await bot.telegram.sendMessage(config.chatId, msg);
1184
+ try {
1185
+ const sent = await bot.telegram.sendMessage(config.chatId, msg);
1138
1186
 
1139
- messageToSession.set(sent.message_id, {
1140
- sessionId: data.session_id,
1141
- type: 'notification',
1142
- createdAt: Date.now(),
1143
- });
1187
+ messageToSession.set(sent.message_id, {
1188
+ sessionId: data.session_id,
1189
+ type: 'notification',
1190
+ createdAt: Date.now(),
1191
+ });
1144
1192
 
1145
- log(`Notification sent: ${notifType} (msg ${sent.message_id})`);
1193
+ log(`Notification sent: ${notifType} (msg ${sent.message_id})`);
1194
+ } catch (err) {
1195
+ log(`Notification send failed: ${err.message}`);
1196
+ }
1146
1197
  }
1147
1198
 
1148
1199
  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
  ],