@inetafrica/open-claudia 3.1.7 → 3.1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## v3.1.8 — Hosted pods can actually send voice notes
4
+
5
+ - **FFMPEG is discovered on PATH.** Provisioned pods ship `/usr/bin/ffmpeg` but never set `FFMPEG=` in config, and `config.js` read only the explicit setting — so `FFMPEG` resolved empty and every TTS route (`pocketToVoice`, ElevenLabs, `say`) silently returned null: voice input got text-only replies. `FFMPEG` now falls back to PATH discovery via the existing `executableCandidate` helper; an explicit `FFMPEG=` still wins.
6
+ - **The system prompt's voice-notes flag reflects real TTS capability.** It gated on `WHISPER_CLI && FFMPEG` — the pre-gateway local-Whisper world — so whisper-less hosted pods told the model "Voice notes: disabled" and it refused user requests (the smart-euvy report), even with working gateway speech. New `media.canSpeak()` mirrors the actual pocket → ElevenLabs → macOS `say` fallback chain and now drives the flag; local Whisper is irrelevant to speaking.
7
+ - **New `open-claudia say "<text>"` CLI verb.** `send-voice` needs a ready-made ogg and the model had no way to synthesize one on request. `say` runs `textToVoice` and sends the result through the loopback voice path (same egress guardrail), and is documented in the Delivery prompt block.
8
+ - Regression coverage in `test-media-speech.js`: a whisper-less pod shape must report `canSpeak() === true`, the prompt gate must key on it, and FFMPEG resolution precedence (explicit > PATH discovery > empty) is asserted via subprocess config loads.
9
+
3
10
  ## v3.1.7 — CI spawn-race hardening (first published build of the v3.1.5 changes)
4
11
 
5
12
  - v3.1.6 also never published. The loaded shared CI runner (node cold-boot 100-370ms vs ~50ms locally) keeps picking off the next-tightest spawn-timing test: this time `test-utility-provider-policy`'s shutdown-drain case, which flat-slept 100ms then cancelled — killing the fake agent before it booted, so its SIGTERM handler never wrote the expected signal capture (pipeline 10383; the v3.1.6 run-lifecycle fix itself passed there).
package/bin/cli.js CHANGED
@@ -277,6 +277,24 @@ switch (command) {
277
277
  break;
278
278
  }
279
279
 
280
+ case "say": {
281
+ const text = args.slice(1).join(" ").trim();
282
+ if (!text) {
283
+ console.error('Usage: open-claudia say "<text>"');
284
+ process.exit(2);
285
+ }
286
+ (async () => {
287
+ const { textToVoice } = require(path.join(botDir, "core", "media"));
288
+ const voicePath = await textToVoice(text);
289
+ if (!voicePath) {
290
+ console.error("No TTS route available — configure the speech gateway, ElevenLabs, or run on macOS.");
291
+ process.exit(1);
292
+ }
293
+ sendViaLoopback("send-voice", [voicePath]);
294
+ })();
295
+ break;
296
+ }
297
+
280
298
  case "tw":
281
299
  case "transcript-window": {
282
300
  require("./transcript-window").run(args.slice(1));
@@ -441,6 +459,7 @@ Send tools (only work inside an active bot-spawned task):
441
459
  open-claudia send-file <path> [caption]
442
460
  open-claudia send-photo <path> [caption]
443
461
  open-claudia send-voice <path>
462
+ open-claudia say "<text>" Synthesize the text (TTS) and send it as a voice note
444
463
 
445
464
  Memory tools:
446
465
  open-claudia transcript-search <query> Ranked FTS5 search over indexed transcripts
package/core/config.js CHANGED
@@ -137,7 +137,9 @@ const TRANSCRIPT_MAX_ENTRY_CHARS = parseInt(config.TRANSCRIPT_MAX_ENTRY_CHARS ||
137
137
  const TRANSCRIPTS_DIR = config.TRANSCRIPTS_DIR || process.env.TRANSCRIPTS_DIR || path.join(CONFIG_DIR, "transcripts");
138
138
  const WHISPER_CLI = config.WHISPER_CLI || "";
139
139
  const WHISPER_MODEL = config.WHISPER_MODEL || "";
140
- const FFMPEG = config.FFMPEG || "";
140
+ // Hosted pods ship /usr/bin/ffmpeg but never set FFMPEG=, so without PATH
141
+ // discovery every TTS route silently no-ops (smart-euvy "voice notes disabled").
142
+ const FFMPEG = config.FFMPEG || executableCandidate("ffmpeg") || "";
141
143
  const ELEVENLABS_API_KEY = config.ELEVENLABS_API_KEY || process.env.ELEVENLABS_API_KEY || "";
142
144
  const ELEVENLABS_VOICE_ID = config.ELEVENLABS_VOICE_ID || process.env.ELEVENLABS_VOICE_ID || "EXAVITQu4vr4xnSDxMaL";
143
145
  const ELEVENLABS_MODEL = config.ELEVENLABS_MODEL || process.env.ELEVENLABS_MODEL || "eleven_v3";
package/core/media.js CHANGED
@@ -129,6 +129,15 @@ async function pocketToVoice(clean) {
129
129
  }
130
130
  }
131
131
 
132
+ // True when at least one textToVoice route can produce audio. Mirrors the
133
+ // pocket -> ElevenLabs -> say fallback chain; local Whisper is deliberately
134
+ // irrelevant here (hosted pods speak via the gateway without it).
135
+ function canSpeak() {
136
+ if (!FFMPEG) return false;
137
+ if (TTS_PROVIDER !== "elevenlabs" && speechBearerToken() && SPEECH_SERVICE_URL) return true;
138
+ return Boolean(ELEVENLABS_API_KEY || TTS_CMD);
139
+ }
140
+
132
141
  // Natural voice via ElevenLabs, falling back to macOS `say` only on no-key/error.
133
142
  async function textToVoice(text) {
134
143
  const clean = cleanForTTS(text);
@@ -194,4 +203,4 @@ async function synthSentenceMp3(text) {
194
203
  }
195
204
  }
196
205
 
197
- module.exports = { transcribeAudio, transcribeAudioLocal, textToVoice, splitSentences, synthSentenceMp3, pocketToVoice, speechBearerToken, TTS_CMD };
206
+ module.exports = { transcribeAudio, transcribeAudioLocal, textToVoice, splitSentences, synthSentenceMp3, pocketToVoice, speechBearerToken, canSpeak, TTS_CMD };
@@ -3,7 +3,8 @@
3
3
 
4
4
  const fs = require("fs");
5
5
  const path = require("path");
6
- const { SOUL_FILE, CRONS_FILE, VAULT_FILE, FILES_DIR, BOT_DIR, WHISPER_CLI, FFMPEG, config } = require("./config");
6
+ const { SOUL_FILE, CRONS_FILE, VAULT_FILE, FILES_DIR, BOT_DIR, config } = require("./config");
7
+ const { canSpeak } = require("./media");
7
8
  const CONFIG_DIR = require("../config-dir");
8
9
  const { currentState, getProjectKey, getProviderSession } = require("./state");
9
10
  const { currentAdapter, currentChannelId, currentUserId } = require("./context");
@@ -198,7 +199,9 @@ ${listing}`;
198
199
 
199
200
  function buildCoreInstructionsUncached() {
200
201
  const soul = loadSoul();
201
- const hasVoice = WHISPER_CLI && FFMPEG;
202
+ // Gate on actual TTS capability, not local Whisper: hosted pods hear and
203
+ // speak through the speech gateway with no whisper binary installed.
204
+ const hasVoice = canSpeak();
202
205
  return `
203
206
  ${soul}
204
207
  ${buildPersonaBlock()}
@@ -230,6 +233,7 @@ Reply normally in your final answer. To send a file, image, or voice clip back t
230
233
  - \`open-claudia send-file <path> [caption]\` — any document/binary
231
234
  - \`open-claudia send-photo <path> [caption]\` — image with inline preview
232
235
  - \`open-claudia send-voice <path>\` — ogg/opus voice note
236
+ - \`open-claudia say "<text>"\` — synthesize the text and send it as a voice note (use this when asked for a voice reply)
233
237
  Never print or embed bot tokens in prompts, commands, logs, or messages.
234
238
 
235
239
  ## Background work
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.1.7",
3
+ "version": "3.1.8",
4
4
  "description": "An always-on, provider-agnostic coding-agent harness for Claude Code and OpenAI Codex via chat",
5
5
  "main": "bot.js",
6
6
  "bin": {
@@ -179,7 +179,14 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
179
179
  // v3.1.6 run-lifecycle fix itself passed there). Audited all 13 fake-agent
180
180
  // test files and hardened the three racy ones (utility-policy, enforcer,
181
181
  // subagent): wait on boot captures / generous deadlines, never tight sleeps.
182
- assert.strictEqual(pkg.version, "3.1.7", "release version must remain unchanged without explicit approval");
182
+ // 3.1.8 approved by Sumeet 2026-07-21 ("Yeh"): pod voice-note fix. Pods ship
183
+ // /usr/bin/ffmpeg but never set FFMPEG=, so every TTS route silently no-opped,
184
+ // and the system-prompt voice flag still gated on local Whisper — hosted bots
185
+ // told users voice notes were disabled despite a working speech gateway.
186
+ // Fix: FFMPEG PATH discovery in config, media.canSpeak() drives the prompt
187
+ // flag, and a new `open-claudia say "<text>"` verb synthesizes + sends a
188
+ // voice note on request.
189
+ assert.strictEqual(pkg.version, "3.1.8", "release version must remain unchanged without explicit approval");
183
190
  for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
184
191
  assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
185
192
  }