@inetafrica/open-claudia 2.6.43 → 2.6.45

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/bot-agent.js CHANGED
@@ -767,25 +767,9 @@ function transcribeAudio(oggPath) {
767
767
  }
768
768
 
769
769
  // ── Text-to-Speech ────────────────────────────────────────────────
770
+ // Shared with direct mode: ElevenLabs natural voice, falling back to `say`.
770
771
 
771
- const TTS_CMD = process.platform === "darwin" ? "say" : null;
772
-
773
- function textToVoice(text) {
774
- if (!TTS_CMD || !FFMPEG) return null;
775
- try {
776
- const clean = text.replace(/[*_`#>\[\]()]/g, "").replace(/\n{2,}/g, ". ").replace(/\n/g, " ").trim();
777
- if (!clean) return null;
778
- const aiffPath = path.join(TEMP_DIR, `tts-${Date.now()}.aiff`);
779
- const oggPath = aiffPath.replace(".aiff", ".ogg");
780
- execSync(`${TTS_CMD} ${JSON.stringify(clean)} -o "${aiffPath}"`, { timeout: 30000 });
781
- execSync(`"${FFMPEG}" -i "${aiffPath}" -c:a libopus -y "${oggPath}" 2>/dev/null`, { timeout: 30000 });
782
- try { fs.unlinkSync(aiffPath); } catch (e) {}
783
- return oggPath;
784
- } catch (e) {
785
- console.error("TTS error:", e.message);
786
- return null;
787
- }
788
- }
772
+ const { textToVoice } = require("./core/media");
789
773
 
790
774
  async function sendVoice(oggPath) {
791
775
  try {
@@ -1421,9 +1405,9 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
1421
1405
  if (code !== 0 && code !== null) await send(`Exit code: ${code}`);
1422
1406
 
1423
1407
  // Send voice reply if input was a voice note
1424
- if (lastInputWasVoice && TTS_CMD) {
1408
+ if (lastInputWasVoice) {
1425
1409
  lastInputWasVoice = false;
1426
- const voicePath = textToVoice(finalText);
1410
+ const voicePath = await textToVoice(finalText);
1427
1411
  if (voicePath) await sendVoice(voicePath);
1428
1412
  }
1429
1413
  } catch (e) {
@@ -19,11 +19,16 @@ class TelegramAdapter {
19
19
  this.token = token;
20
20
  this.ownerChatId = ownerChatId;
21
21
  this.chatIds = chatIds || [];
22
+ // Own the HTTP(S) agent so we can drop dead keep-alive sockets on reconnect.
23
+ this._agent = new https.Agent({ keepAlive: true });
22
24
  this.bot = new TelegramBot(token, {
23
25
  polling: { autoStart: false, params: { timeout: 30 } },
26
+ request: { agent: this._agent },
24
27
  });
25
28
  this._listeners = { message: new Set(), action: new Set() };
26
29
  this._reconnectTimer = null;
30
+ this._recoveryTimer = null;
31
+ this._reconnectAttempts = 0;
27
32
  this._wireInbound();
28
33
  }
29
34
 
@@ -45,9 +50,49 @@ class TelegramAdapter {
45
50
  }
46
51
 
47
52
  async stop() {
53
+ if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null; }
54
+ if (this._recoveryTimer) { clearTimeout(this._recoveryTimer); this._recoveryTimer = null; }
48
55
  try { await this.bot.stopPolling(); } catch (e) {}
49
56
  }
50
57
 
58
+ // Recover from a wedged poll loop. The library reuses one keep-alive socket;
59
+ // after a sleep/wake or network blip that socket dies and every poll then
60
+ // ETIMEDOUTs forever — restarting polling alone reuses the same dead socket.
61
+ // So we destroy the agent (forcing a fresh connection), back off, and if we
62
+ // still can't recover after several tries we exit so launchd (KeepAlive)
63
+ // relaunches a fully clean process.
64
+ _scheduleReconnect() {
65
+ if (this._reconnectTimer) return; // one cycle in flight; swallow the flood
66
+ if (this._recoveryTimer) { clearTimeout(this._recoveryTimer); this._recoveryTimer = null; }
67
+ this._reconnectAttempts += 1;
68
+ const MAX = 6;
69
+ if (this._reconnectAttempts > MAX) {
70
+ console.error(`Polling unrecoverable after ${MAX} attempts — exiting for supervisor restart.`);
71
+ process.exit(1);
72
+ }
73
+ const delay = Math.min(30000, 1000 * 2 ** this._reconnectAttempts); // 2s,4s,8s,16s,30s,30s
74
+ console.log(`Network lost. Reconnecting in ${Math.round(delay / 1000)}s (attempt ${this._reconnectAttempts}/${MAX})...`);
75
+ this._reconnectTimer = setTimeout(async () => {
76
+ this._reconnectTimer = null;
77
+ try {
78
+ try { await this.bot.stopPolling(); } catch (e) {}
79
+ try { this._agent.destroy(); } catch (e) {} // drop the dead pooled socket
80
+ await new Promise((r) => setTimeout(r, 1000));
81
+ await this.bot.startPolling();
82
+ console.log("Polling restarted.");
83
+ // No fresh error within 30s ⇒ genuinely recovered; reset the counter.
84
+ this._recoveryTimer = setTimeout(() => {
85
+ this._recoveryTimer = null;
86
+ if (this._reconnectAttempts) console.log("Connection healthy again.");
87
+ this._reconnectAttempts = 0;
88
+ }, 30000);
89
+ } catch (e) {
90
+ console.error("Reconnect attempt failed:", e.message);
91
+ this._scheduleReconnect(); // back off and retry; don't die on first miss
92
+ }
93
+ }, delay);
94
+ }
95
+
51
96
  _wireInbound() {
52
97
  this.bot.on("polling_error", (err) => {
53
98
  const msg = err.message || "";
@@ -56,21 +101,8 @@ class TelegramAdapter {
56
101
  console.error("Another instance is polling. Exiting.");
57
102
  process.exit(1);
58
103
  }
59
- if (msg.includes("ETIMEDOUT") || msg.includes("ECONNRESET") || msg.includes("ENOTFOUND") || msg.includes("EFATAL")) {
60
- if (this._reconnectTimer) return;
61
- console.log("Network lost. Reconnecting in 10s...");
62
- this._reconnectTimer = setTimeout(async () => {
63
- this._reconnectTimer = null;
64
- try {
65
- await this.bot.stopPolling();
66
- await new Promise((r) => setTimeout(r, 2000));
67
- await this.bot.startPolling();
68
- console.log("Reconnected.");
69
- } catch (e) {
70
- console.error("Reconnect failed:", e.message);
71
- process.exit(1);
72
- }
73
- }, 10000);
104
+ if (/ETIMEDOUT|ECONNRESET|ENOTFOUND|ENETUNREACH|EAI_AGAIN|EFATAL|socket hang up/i.test(msg)) {
105
+ this._scheduleReconnect();
74
106
  }
75
107
  });
76
108
 
package/core/config.js CHANGED
@@ -99,6 +99,9 @@ const TRANSCRIPTS_DIR = config.TRANSCRIPTS_DIR || process.env.TRANSCRIPTS_DIR ||
99
99
  const WHISPER_CLI = config.WHISPER_CLI || "";
100
100
  const WHISPER_MODEL = config.WHISPER_MODEL || "";
101
101
  const FFMPEG = config.FFMPEG || "";
102
+ const ELEVENLABS_API_KEY = config.ELEVENLABS_API_KEY || process.env.ELEVENLABS_API_KEY || "";
103
+ const ELEVENLABS_VOICE_ID = config.ELEVENLABS_VOICE_ID || process.env.ELEVENLABS_VOICE_ID || "EXAVITQu4vr4xnSDxMaL";
104
+ const ELEVENLABS_MODEL = config.ELEVENLABS_MODEL || process.env.ELEVENLABS_MODEL || "eleven_v3";
102
105
  const SOUL_FILE = config.SOUL_FILE || path.join(CONFIG_DIR, "soul.md");
103
106
  const CRONS_FILE = config.CRONS_FILE || path.join(CONFIG_DIR, "crons.json");
104
107
  const JOBS_FILE = config.JOBS_FILE || path.join(CONFIG_DIR, "jobs.json");
@@ -233,6 +236,7 @@ module.exports = {
233
236
  TRANSCRIPT_MAX_ENTRY_CHARS,
234
237
  TRANSCRIPTS_DIR,
235
238
  WHISPER_CLI, WHISPER_MODEL, FFMPEG,
239
+ ELEVENLABS_API_KEY, ELEVENLABS_VOICE_ID, ELEVENLABS_MODEL,
236
240
  SOUL_FILE, CRONS_FILE, JOBS_FILE, TASKS_DIR, VAULT_FILE, AUTH_FILE, IDENTITIES_FILE,
237
241
  PEOPLE_FILE, INTROS_FILE, AUDIT_FILE,
238
242
  STATE_FILE, SESSIONS_FILE,
package/core/media.js CHANGED
@@ -4,7 +4,7 @@
4
4
  const fs = require("fs");
5
5
  const path = require("path");
6
6
  const { execSync } = require("child_process");
7
- const { WHISPER_CLI, WHISPER_MODEL, FFMPEG, TEMP_DIR } = require("./config");
7
+ const { WHISPER_CLI, WHISPER_MODEL, FFMPEG, TEMP_DIR, ELEVENLABS_API_KEY, ELEVENLABS_VOICE_ID, ELEVENLABS_MODEL } = require("./config");
8
8
 
9
9
  const TTS_CMD = process.platform === "darwin" ? "say" : null;
10
10
 
@@ -19,11 +19,14 @@ function transcribeAudio(oggPath) {
19
19
  .join(" ").trim();
20
20
  }
21
21
 
22
- function textToVoice(text) {
22
+ function cleanForTTS(text) {
23
+ return text.replace(/[*_`#>\[\]()]/g, "").replace(/\n{2,}/g, ". ").replace(/\n/g, " ").trim();
24
+ }
25
+
26
+ // macOS `say` fallback. Synchronous. Returns ogg path or null.
27
+ function sayToVoice(clean) {
23
28
  if (!TTS_CMD || !FFMPEG) return null;
24
29
  try {
25
- const clean = text.replace(/[*_`#>\[\]()]/g, "").replace(/\n{2,}/g, ". ").replace(/\n/g, " ").trim();
26
- if (!clean) return null;
27
30
  const aiffPath = path.join(TEMP_DIR, `tts-${Date.now()}.aiff`);
28
31
  const oggPath = aiffPath.replace(".aiff", ".ogg");
29
32
  execSync(`${TTS_CMD} ${JSON.stringify(clean)} -o "${aiffPath}"`, { timeout: 30000 });
@@ -31,9 +34,49 @@ function textToVoice(text) {
31
34
  try { fs.unlinkSync(aiffPath); } catch (e) {}
32
35
  return oggPath;
33
36
  } catch (e) {
34
- console.error("TTS error:", e.message);
37
+ console.error("say TTS error:", e.message);
35
38
  return null;
36
39
  }
37
40
  }
38
41
 
42
+ // Natural TTS via ElevenLabs. Returns ogg path or null on any failure.
43
+ async function elevenLabsToVoice(clean) {
44
+ if (!ELEVENLABS_API_KEY || !FFMPEG) return null;
45
+ try {
46
+ const res = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${ELEVENLABS_VOICE_ID}`, {
47
+ method: "POST",
48
+ headers: { "xi-api-key": ELEVENLABS_API_KEY, "Content-Type": "application/json" },
49
+ body: JSON.stringify({
50
+ text: clean,
51
+ model_id: ELEVENLABS_MODEL,
52
+ voice_settings: { stability: 0.5, similarity_boost: 0.85, style: 0.5, use_speaker_boost: true },
53
+ }),
54
+ });
55
+ if (!res.ok) {
56
+ const body = await res.text().catch(() => "");
57
+ console.error(`ElevenLabs TTS failed: ${res.status} ${body}`.slice(0, 300));
58
+ return null;
59
+ }
60
+ const buf = Buffer.from(await res.arrayBuffer());
61
+ const mp3Path = path.join(TEMP_DIR, `tts-${Date.now()}.mp3`);
62
+ const oggPath = mp3Path.replace(".mp3", ".ogg");
63
+ fs.writeFileSync(mp3Path, buf);
64
+ execSync(`"${FFMPEG}" -i "${mp3Path}" -c:a libopus -y "${oggPath}" 2>/dev/null`, { timeout: 30000 });
65
+ try { fs.unlinkSync(mp3Path); } catch (e) {}
66
+ return oggPath;
67
+ } catch (e) {
68
+ console.error("ElevenLabs TTS error:", e.message);
69
+ return null;
70
+ }
71
+ }
72
+
73
+ // Natural voice via ElevenLabs, falling back to macOS `say` only on no-key/error.
74
+ async function textToVoice(text) {
75
+ const clean = cleanForTTS(text);
76
+ if (!clean) return null;
77
+ const eleven = await elevenLabsToVoice(clean);
78
+ if (eleven) return eleven;
79
+ return sayToVoice(clean);
80
+ }
81
+
39
82
  module.exports = { transcribeAudio, textToVoice, TTS_CMD };
package/core/runner.js CHANGED
@@ -16,7 +16,7 @@ const { chatContext, currentChannelId, currentAdapter } = require("./context");
16
16
  const { buildSystemPrompt, promptWithDynamicContext } = require("./system-prompt");
17
17
  const { redactSensitive } = require("./redact");
18
18
  const { send, editMessage, sendVoice, splitMessage } = require("./io");
19
- const { textToVoice, TTS_CMD } = require("./media");
19
+ const { textToVoice } = require("./media");
20
20
  const { killProcessTree } = require("./process-tree");
21
21
  const {
22
22
  appendProjectTranscript, transcriptProjectInfo,
@@ -1193,9 +1193,9 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
1193
1193
  }
1194
1194
  if (code !== 0 && code !== null) await send(`Exit code: ${code}`);
1195
1195
 
1196
- if (state.lastInputWasVoice && TTS_CMD) {
1196
+ if (state.lastInputWasVoice) {
1197
1197
  state.lastInputWasVoice = false;
1198
- const voicePath = textToVoice(finalText);
1198
+ const voicePath = await textToVoice(finalText);
1199
1199
  if (voicePath) await sendVoice(voicePath);
1200
1200
  }
1201
1201
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.6.43",
3
+ "version": "2.6.45",
4
4
  "description": "Your always-on AI coding assistant — Claude Code, Cursor Agent, and OpenAI Codex via Telegram or Kazee Chat",
5
5
  "main": "bot.js",
6
6
  "bin": {