@rubytech/taskmaster 1.2.1 → 1.3.0

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.
Files changed (64) hide show
  1. package/dist/agents/auth-profiles/profiles.js +37 -0
  2. package/dist/agents/auth-profiles.js +1 -1
  3. package/dist/agents/pi-tools.policy.js +4 -0
  4. package/dist/agents/taskmaster-tools.js +14 -0
  5. package/dist/agents/tool-policy.js +5 -2
  6. package/dist/agents/tools/apikeys-tool.js +16 -5
  7. package/dist/agents/tools/contact-create-tool.js +59 -0
  8. package/dist/agents/tools/contact-delete-tool.js +48 -0
  9. package/dist/agents/tools/contact-update-tool.js +17 -2
  10. package/dist/agents/tools/file-delete-tool.js +137 -0
  11. package/dist/agents/tools/file-list-tool.js +127 -0
  12. package/dist/auto-reply/reply/commands-tts.js +7 -2
  13. package/dist/build-info.json +3 -3
  14. package/dist/cli/provision-seed.js +1 -2
  15. package/dist/commands/doctor-config-flow.js +13 -0
  16. package/dist/config/agent-tools-reconcile.js +53 -0
  17. package/dist/config/defaults.js +10 -1
  18. package/dist/config/legacy.migrations.part-3.js +26 -0
  19. package/dist/config/zod-schema.core.js +9 -1
  20. package/dist/config/zod-schema.js +1 -0
  21. package/dist/control-ui/assets/index-CPawOl_z.css +1 -0
  22. package/dist/control-ui/assets/{index-N8du4fwV.js → index-DQ1kxYd4.js} +692 -598
  23. package/dist/control-ui/assets/index-DQ1kxYd4.js.map +1 -0
  24. package/dist/control-ui/index.html +2 -2
  25. package/dist/gateway/config-reload.js +1 -0
  26. package/dist/gateway/media-http.js +28 -0
  27. package/dist/gateway/server-methods/apikeys.js +56 -4
  28. package/dist/gateway/server-methods/tts.js +11 -2
  29. package/dist/gateway/server.impl.js +15 -0
  30. package/dist/media-understanding/apply.js +35 -0
  31. package/dist/media-understanding/providers/deepgram/audio.js +1 -1
  32. package/dist/media-understanding/providers/google/audio.js +1 -1
  33. package/dist/media-understanding/providers/google/video.js +1 -1
  34. package/dist/media-understanding/providers/index.js +2 -0
  35. package/dist/media-understanding/providers/openai/audio.js +1 -1
  36. package/dist/media-understanding/providers/sherpa-onnx/index.js +10 -0
  37. package/dist/media-understanding/runner.js +61 -72
  38. package/dist/media-understanding/sherpa-onnx-local.js +223 -0
  39. package/dist/records/records-manager.js +10 -0
  40. package/dist/tts/tts.js +98 -10
  41. package/dist/web/auto-reply/monitor/process-message.js +1 -0
  42. package/dist/web/inbound/monitor.js +9 -1
  43. package/extensions/googlechat/node_modules/.bin/taskmaster +2 -2
  44. package/extensions/googlechat/package.json +2 -2
  45. package/extensions/line/node_modules/.bin/taskmaster +2 -2
  46. package/extensions/line/package.json +1 -1
  47. package/extensions/matrix/node_modules/.bin/taskmaster +2 -2
  48. package/extensions/matrix/package.json +1 -1
  49. package/extensions/msteams/node_modules/.bin/taskmaster +2 -2
  50. package/extensions/msteams/package.json +1 -1
  51. package/extensions/nostr/node_modules/.bin/taskmaster +2 -2
  52. package/extensions/nostr/package.json +1 -1
  53. package/extensions/zalo/node_modules/.bin/taskmaster +2 -2
  54. package/extensions/zalo/package.json +1 -1
  55. package/extensions/zalouser/node_modules/.bin/taskmaster +2 -2
  56. package/extensions/zalouser/package.json +1 -1
  57. package/package.json +3 -2
  58. package/scripts/postinstall.js +76 -0
  59. package/skills/business-assistant/references/crm.md +32 -8
  60. package/taskmaster-docs/USER-GUIDE.md +84 -5
  61. package/templates/beagle/agents/admin/AGENTS.md +4 -2
  62. package/templates/taskmaster/agents/admin/AGENTS.md +1 -0
  63. package/dist/control-ui/assets/index-DtQHRIVD.css +0 -1
  64. package/dist/control-ui/assets/index-N8du4fwV.js.map +0 -1
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Local speech-to-text using sherpa-onnx-node (optional dependency).
3
+ *
4
+ * Model: NeMo CTC English Conformer Small (int8)
5
+ * Storage: ~/.taskmaster/lib/sherpa-onnx/nemo-ctc-en-conformer-small/
6
+ *
7
+ * Requires: sherpa-onnx-node (optional dep), ffmpeg (for OGG→WAV conversion)
8
+ */
9
+ import { execFile as execFileCb, spawnSync } from "node:child_process";
10
+ import fs from "node:fs/promises";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import { promisify } from "node:util";
14
+ const execFile = promisify(execFileCb);
15
+ // ---------------------------------------------------------------------------
16
+ // Constants
17
+ // ---------------------------------------------------------------------------
18
+ const LIB_DIR = path.join(os.homedir(), ".taskmaster", "lib", "sherpa-onnx");
19
+ const MODEL_DIR_NAME = "nemo-ctc-en-conformer-small";
20
+ const MODEL_DIR = path.join(LIB_DIR, MODEL_DIR_NAME);
21
+ const MODEL_FILE = "model.int8.onnx";
22
+ const TOKENS_FILE = "tokens.txt";
23
+ const MODEL_ARCHIVE_URL = "https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-nemo-ctc-en-conformer-small.tar.bz2";
24
+ const ARCHIVE_INNER_DIR = "sherpa-onnx-nemo-ctc-en-conformer-small";
25
+ export const MODEL_LABEL = "sherpa-onnx/nemo-ctc-en-conformer-small-int8";
26
+ let sherpaModule = null;
27
+ let sherpaModuleError = null;
28
+ async function importSherpaOnnx() {
29
+ if (sherpaModule)
30
+ return sherpaModule;
31
+ if (sherpaModuleError)
32
+ throw sherpaModuleError;
33
+ try {
34
+ const mod = await import("sherpa-onnx-node");
35
+ // ESM dynamic import puts named exports under .default
36
+ sherpaModule = (mod.default ?? mod);
37
+ return sherpaModule;
38
+ }
39
+ catch (err) {
40
+ sherpaModuleError = err instanceof Error ? err : new Error(String(err));
41
+ throw sherpaModuleError;
42
+ }
43
+ }
44
+ // ---------------------------------------------------------------------------
45
+ // Availability checks
46
+ // ---------------------------------------------------------------------------
47
+ let ffmpegAvailable = null;
48
+ function checkFfmpeg() {
49
+ if (ffmpegAvailable !== null)
50
+ return ffmpegAvailable;
51
+ const result = spawnSync("ffmpeg", ["-version"], { stdio: "ignore", timeout: 5_000 });
52
+ ffmpegAvailable = result.status === 0;
53
+ return ffmpegAvailable;
54
+ }
55
+ async function fileExists(filePath) {
56
+ try {
57
+ await fs.stat(filePath);
58
+ return true;
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
64
+ export async function isModelDownloaded() {
65
+ return ((await fileExists(path.join(MODEL_DIR, MODEL_FILE))) &&
66
+ (await fileExists(path.join(MODEL_DIR, TOKENS_FILE))));
67
+ }
68
+ /**
69
+ * Returns true when both the npm package and ffmpeg are available.
70
+ * Does NOT check whether the model is downloaded (use isReady for that).
71
+ */
72
+ export async function isAvailable() {
73
+ try {
74
+ await importSherpaOnnx();
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ return checkFfmpeg();
80
+ }
81
+ /**
82
+ * Returns true when everything is ready for immediate transcription:
83
+ * npm package + ffmpeg + model files on disk.
84
+ */
85
+ export async function isReady() {
86
+ return (await isAvailable()) && (await isModelDownloaded());
87
+ }
88
+ // ---------------------------------------------------------------------------
89
+ // Model download
90
+ // ---------------------------------------------------------------------------
91
+ let downloadInFlight = null;
92
+ /**
93
+ * Download and extract the model if not already present.
94
+ * Deduplicates concurrent calls.
95
+ */
96
+ export async function ensureModel() {
97
+ if (await isModelDownloaded())
98
+ return MODEL_DIR;
99
+ if (!downloadInFlight) {
100
+ downloadInFlight = downloadModelOnce().finally(() => {
101
+ downloadInFlight = null;
102
+ });
103
+ }
104
+ await downloadInFlight;
105
+ return MODEL_DIR;
106
+ }
107
+ async function downloadModelOnce() {
108
+ console.log("[sherpa-onnx] Downloading speech recognition model…");
109
+ await fs.mkdir(LIB_DIR, { recursive: true });
110
+ const archivePath = path.join(LIB_DIR, `${MODEL_DIR_NAME}.tar.bz2`);
111
+ try {
112
+ // Download archive using curl (available on macOS and Linux)
113
+ await execFile("curl", ["-fsSL", "-o", archivePath, MODEL_ARCHIVE_URL], {
114
+ timeout: 300_000,
115
+ });
116
+ // Extract — archive contains sherpa-onnx-nemo-ctc-en-conformer-small/{files}
117
+ await execFile("tar", ["-xjf", archivePath, "-C", LIB_DIR], {
118
+ timeout: 120_000,
119
+ });
120
+ // Rename extracted directory to our standard name (remove sherpa-onnx- prefix)
121
+ const extractedDir = path.join(LIB_DIR, ARCHIVE_INNER_DIR);
122
+ if ((await fileExists(extractedDir)) && extractedDir !== MODEL_DIR) {
123
+ // If target already exists (partial state), remove it first
124
+ if (await fileExists(MODEL_DIR)) {
125
+ await fs.rm(MODEL_DIR, { recursive: true });
126
+ }
127
+ await fs.rename(extractedDir, MODEL_DIR);
128
+ }
129
+ // Clean up: remove archive and unnecessary files to save disk space
130
+ await fs.unlink(archivePath).catch(() => { });
131
+ // Remove full-precision model (keep only int8 — saves ~37MB)
132
+ await fs.unlink(path.join(MODEL_DIR, "model.onnx")).catch(() => { });
133
+ // Remove test wav files
134
+ await fs
135
+ .rm(path.join(MODEL_DIR, "test_wavs"), { recursive: true, force: true })
136
+ .catch(() => { });
137
+ console.log("[sherpa-onnx] Model installed");
138
+ }
139
+ catch (err) {
140
+ // Clean up partial download
141
+ await fs.unlink(archivePath).catch(() => { });
142
+ await fs.rm(MODEL_DIR, { recursive: true, force: true }).catch(() => { });
143
+ throw new Error(`sherpa-onnx model download failed: ${String(err)}`);
144
+ }
145
+ }
146
+ // ---------------------------------------------------------------------------
147
+ // Audio conversion (OGG/Opus → WAV 16kHz mono)
148
+ // ---------------------------------------------------------------------------
149
+ async function convertToWav(inputPath, outputPath) {
150
+ await execFile("ffmpeg", [
151
+ "-i",
152
+ inputPath,
153
+ "-ar",
154
+ "16000",
155
+ "-ac",
156
+ "1",
157
+ "-sample_fmt",
158
+ "s16",
159
+ "-f",
160
+ "wav",
161
+ "-y",
162
+ outputPath,
163
+ ], { timeout: 30_000 });
164
+ }
165
+ // ---------------------------------------------------------------------------
166
+ // Recognizer (cached singleton)
167
+ // ---------------------------------------------------------------------------
168
+ let recognizer = null;
169
+ async function getRecognizer() {
170
+ if (recognizer)
171
+ return recognizer;
172
+ const sherpa = await importSherpaOnnx();
173
+ const modelDir = await ensureModel();
174
+ const config = {
175
+ featConfig: { sampleRate: 16000, featureDim: 80 },
176
+ modelConfig: {
177
+ nemoCtc: {
178
+ model: path.join(modelDir, MODEL_FILE),
179
+ },
180
+ tokens: path.join(modelDir, TOKENS_FILE),
181
+ numThreads: 2,
182
+ provider: "cpu",
183
+ debug: 0,
184
+ },
185
+ };
186
+ recognizer = new sherpa.OfflineRecognizer(config);
187
+ return recognizer;
188
+ }
189
+ // ---------------------------------------------------------------------------
190
+ // Public transcription API
191
+ // ---------------------------------------------------------------------------
192
+ /**
193
+ * Transcribe an audio buffer (any format ffmpeg can read) to text.
194
+ * Returns the transcription text, or throws on failure.
195
+ */
196
+ export async function transcribeLocal(buffer, fileName) {
197
+ const sherpa = await importSherpaOnnx();
198
+ const rec = await getRecognizer();
199
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "sherpa-stt-"));
200
+ try {
201
+ const inputPath = path.join(tmpDir, fileName || "audio.ogg");
202
+ await fs.writeFile(inputPath, buffer);
203
+ const wavPath = path.join(tmpDir, "audio.wav");
204
+ await convertToWav(inputPath, wavPath);
205
+ const wave = sherpa.readWave(wavPath);
206
+ const stream = rec.createStream();
207
+ stream.acceptWaveform({ sampleRate: wave.sampleRate, samples: wave.samples });
208
+ rec.decode(stream);
209
+ const result = rec.getResult(stream);
210
+ const text = result?.text?.trim() ?? "";
211
+ if (!text) {
212
+ throw new Error("sherpa-onnx returned empty transcription");
213
+ }
214
+ return { text, model: MODEL_LABEL };
215
+ }
216
+ finally {
217
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
218
+ }
219
+ }
220
+ // ---------------------------------------------------------------------------
221
+ // Exported paths (for postinstall and diagnostics)
222
+ // ---------------------------------------------------------------------------
223
+ export { LIB_DIR, MODEL_DIR, MODEL_DIR_NAME, MODEL_FILE, TOKENS_FILE, MODEL_ARCHIVE_URL, ARCHIVE_INNER_DIR, };
@@ -90,3 +90,13 @@ export function deleteRecordField(id, key) {
90
90
  writeFile(data);
91
91
  return record;
92
92
  }
93
+ export function setRecordName(id, name) {
94
+ const data = readFile();
95
+ const record = data.records[id];
96
+ if (!record)
97
+ return null;
98
+ record.name = name;
99
+ record.updatedAt = new Date().toISOString();
100
+ writeFile(data);
101
+ return record;
102
+ }
package/dist/tts/tts.js CHANGED
@@ -5,11 +5,13 @@ import { completeSimple } from "@mariozechner/pi-ai";
5
5
  import { EdgeTTS } from "node-edge-tts";
6
6
  import { normalizeChannelId } from "../channels/plugins/index.js";
7
7
  import { logVerbose } from "../globals.js";
8
+ import { createSubsystemLogger } from "../logging/subsystem.js";
8
9
  import { isVoiceCompatibleAudio } from "../media/audio.js";
9
10
  import { CONFIG_DIR, resolveUserPath } from "../utils.js";
10
- import { getApiKeyForModel, requireApiKey } from "../agents/model-auth.js";
11
+ import { getApiKeyForModel, requireApiKey, resolveApiKeyForProvider, } from "../agents/model-auth.js";
11
12
  import { buildModelAliasIndex, resolveDefaultModelForAgent, resolveModelRefFromString, } from "../agents/model-selection.js";
12
13
  import { resolveModel } from "../agents/pi-embedded-runner/model.js";
14
+ const log = createSubsystemLogger("gateway/tts");
13
15
  const DEFAULT_TIMEOUT_MS = 30_000;
14
16
  const DEFAULT_TTS_MAX_LENGTH = 1500;
15
17
  const DEFAULT_TTS_SUMMARIZE = true;
@@ -133,6 +135,11 @@ export function resolveTtsConfig(cfg) {
133
135
  proxy: raw.edge?.proxy?.trim() || undefined,
134
136
  timeoutMs: raw.edge?.timeoutMs,
135
137
  },
138
+ hume: {
139
+ apiKey: raw.hume?.apiKey,
140
+ voice: raw.hume?.voice?.trim() || "KORA",
141
+ description: raw.hume?.description?.trim() || undefined,
142
+ },
136
143
  prefsPath: raw.prefsPath,
137
144
  maxTextLength: raw.maxTextLength ?? DEFAULT_MAX_TEXT_LENGTH,
138
145
  timeoutMs: raw.timeoutMs ?? DEFAULT_TIMEOUT_MS,
@@ -173,15 +180,15 @@ export function buildTtsSystemPromptHint(cfg) {
173
180
  const maxLength = getTtsMaxLength(prefsPath);
174
181
  const summarize = isSummarizationEnabled(prefsPath) ? "on" : "off";
175
182
  const autoHint = autoMode === "inbound"
176
- ? "Only use TTS when the user's last message includes audio/voice."
183
+ ? "Your replies are automatically converted to voice notes when the user sends audio/voice."
177
184
  : autoMode === "tagged"
178
- ? "Only use TTS when you include [[tts]] or [[tts:text]] tags."
179
- : undefined;
185
+ ? "Your replies are converted to voice notes when you include [[tts]] or [[tts:text]] tags."
186
+ : "Your replies are automatically converted to voice notes and sent as audio messages.";
180
187
  return [
181
- "Voice (TTS) is enabled.",
188
+ "Voice replies are enabled — you CAN send voice notes.",
182
189
  autoHint,
190
+ "Write naturally as if speaking. Do not tell the user you cannot send voice messages — you can.",
183
191
  `Keep spoken text ≤${maxLength} chars to avoid auto-summary (summary ${summarize}).`,
184
- "Use [[tts:...]] and optional [[tts:text]]...[[/tts:text]] to control voice/expressiveness.",
185
192
  ]
186
193
  .filter(Boolean)
187
194
  .join("\n");
@@ -291,15 +298,20 @@ export function resolveTtsApiKey(config, provider) {
291
298
  if (provider === "openai") {
292
299
  return config.openai.apiKey || process.env.OPENAI_API_KEY;
293
300
  }
301
+ if (provider === "hume") {
302
+ return config.hume.apiKey || process.env.HUME_API_KEY;
303
+ }
294
304
  return undefined;
295
305
  }
296
- export const TTS_PROVIDERS = ["openai", "elevenlabs", "edge"];
306
+ export const TTS_PROVIDERS = ["openai", "elevenlabs", "hume", "edge"];
297
307
  export function resolveTtsProviderOrder(primary) {
298
308
  return [primary, ...TTS_PROVIDERS.filter((provider) => provider !== primary)];
299
309
  }
300
310
  export function isTtsProviderConfigured(config, provider) {
301
311
  if (provider === "edge")
302
312
  return config.edge.enabled;
313
+ if (provider === "hume")
314
+ return Boolean(resolveTtsApiKey(config, provider));
303
315
  return Boolean(resolveTtsApiKey(config, provider));
304
316
  }
305
317
  function isValidVoiceId(voiceId) {
@@ -350,6 +362,14 @@ function normalizeSeed(seed) {
350
362
  }
351
363
  return next;
352
364
  }
365
+ /** Strip emoji characters so TTS engines don't read them aloud by name. */
366
+ function stripEmoji(text) {
367
+ // Unicode emoji ranges: emoticons, dingbats, symbols, flags, components
368
+ return text
369
+ .replace(/[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu, "")
370
+ .replace(/ +/g, " ")
371
+ .trim();
372
+ }
353
373
  function parseBooleanValue(value) {
354
374
  const normalized = value.trim().toLowerCase();
355
375
  if (["true", "1", "yes", "on"].includes(normalized))
@@ -777,6 +797,50 @@ async function openaiTTS(params) {
777
797
  clearTimeout(timeout);
778
798
  }
779
799
  }
800
+ // ---------------------------------------------------------------------------
801
+ // Hume Octave TTS — REST API
802
+ // POST https://api.hume.ai/v0/tts
803
+ // ---------------------------------------------------------------------------
804
+ const HUME_TTS_BASE_URL = "https://api.hume.ai/v0/tts";
805
+ async function humeTTS(params) {
806
+ const { text, apiKey, voice, description, timeoutMs } = params;
807
+ const controller = new AbortController();
808
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
809
+ try {
810
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(voice);
811
+ const voiceSpec = isUuid ? { id: voice } : { name: voice, provider: "HUME_AI" };
812
+ const utterance = { text, voice: voiceSpec };
813
+ if (description) {
814
+ utterance.description = description;
815
+ }
816
+ const response = await fetch(HUME_TTS_BASE_URL, {
817
+ method: "POST",
818
+ headers: {
819
+ "X-Hume-Api-Key": apiKey,
820
+ "Content-Type": "application/json",
821
+ },
822
+ body: JSON.stringify({
823
+ utterances: [utterance],
824
+ format: { type: "mp3" },
825
+ strip_headers: true,
826
+ }),
827
+ signal: controller.signal,
828
+ });
829
+ if (!response.ok) {
830
+ const body = await response.text().catch(() => "");
831
+ throw new Error(`Hume TTS API error (${response.status}): ${body.slice(0, 200)}`);
832
+ }
833
+ const json = (await response.json());
834
+ const audio64 = json.generations?.[0]?.audio;
835
+ if (!audio64) {
836
+ throw new Error("Hume TTS: no audio in response");
837
+ }
838
+ return Buffer.from(audio64, "base64");
839
+ }
840
+ finally {
841
+ clearTimeout(timeout);
842
+ }
843
+ }
780
844
  function inferEdgeExtension(outputFormat) {
781
845
  const normalized = outputFormat.toLowerCase();
782
846
  if (normalized.includes("webm"))
@@ -888,7 +952,17 @@ export async function textToSpeech(params) {
888
952
  voiceCompatible,
889
953
  };
890
954
  }
891
- const apiKey = resolveTtsApiKey(config, provider);
955
+ let apiKey = resolveTtsApiKey(config, provider);
956
+ // Hume keys are stored in auth profiles (API Keys UI). Fall back to async lookup.
957
+ if (!apiKey && provider === "hume") {
958
+ try {
959
+ const auth = await resolveApiKeyForProvider({ provider: "hume", cfg: params.cfg });
960
+ apiKey = auth.apiKey?.trim() || undefined;
961
+ }
962
+ catch {
963
+ // No auth profile key available
964
+ }
965
+ }
892
966
  if (!apiKey) {
893
967
  lastError = `No API key for ${provider}`;
894
968
  continue;
@@ -918,6 +992,15 @@ export async function textToSpeech(params) {
918
992
  timeoutMs: config.timeoutMs,
919
993
  });
920
994
  }
995
+ else if (provider === "hume") {
996
+ audioBuffer = await humeTTS({
997
+ text: params.text,
998
+ apiKey,
999
+ voice: config.hume.voice,
1000
+ description: config.hume.description,
1001
+ timeoutMs: config.timeoutMs,
1002
+ });
1003
+ }
921
1004
  else {
922
1005
  const openaiModelOverride = params.overrides?.openai?.model;
923
1006
  const openaiVoiceOverride = params.overrides?.openai?.voice;
@@ -931,6 +1014,7 @@ export async function textToSpeech(params) {
931
1014
  });
932
1015
  }
933
1016
  const latencyMs = Date.now() - providerStart;
1017
+ log.info(`success via ${provider} (${latencyMs}ms, ${audioBuffer.length} bytes)`);
934
1018
  const tempDir = mkdtempSync(path.join(tmpdir(), "tts-"));
935
1019
  const audioPath = path.join(tempDir, `voice-${Date.now()}${output.extension}`);
936
1020
  writeFileSync(audioPath, audioBuffer);
@@ -940,7 +1024,7 @@ export async function textToSpeech(params) {
940
1024
  audioPath,
941
1025
  latencyMs,
942
1026
  provider,
943
- outputFormat: provider === "openai" ? output.openai : output.elevenlabs,
1027
+ outputFormat: provider === "openai" ? output.openai : provider === "hume" ? "mp3" : output.elevenlabs,
944
1028
  voiceCompatible: output.voiceCompatible,
945
1029
  };
946
1030
  }
@@ -952,8 +1036,10 @@ export async function textToSpeech(params) {
952
1036
  else {
953
1037
  lastError = `${provider}: ${error.message}`;
954
1038
  }
1039
+ log.error(`${provider} failed: ${lastError}`);
955
1040
  }
956
1041
  }
1042
+ log.error(`all providers failed: ${lastError || "none available"}`);
957
1043
  return {
958
1044
  success: false,
959
1045
  error: `TTS conversion failed: ${lastError || "no providers available"}`,
@@ -1033,8 +1119,10 @@ export async function textToSpeechTelephony(params) {
1033
1119
  else {
1034
1120
  lastError = `${provider}: ${error.message}`;
1035
1121
  }
1122
+ log.error(`telephony: ${provider} failed: ${lastError}`);
1036
1123
  }
1037
1124
  }
1125
+ log.error(`telephony: all providers failed: ${lastError || "none available"}`);
1038
1126
  return {
1039
1127
  success: false,
1040
1128
  error: `TTS conversion failed: ${lastError || "no providers available"}`,
@@ -1081,7 +1169,7 @@ export async function maybeApplyTtsToPayload(params) {
1081
1169
  if (ttsText.trim().length < 10)
1082
1170
  return nextPayload;
1083
1171
  const maxLength = getTtsMaxLength(prefsPath);
1084
- let textForAudio = ttsText.trim();
1172
+ let textForAudio = stripEmoji(ttsText.trim());
1085
1173
  let wasSummarized = false;
1086
1174
  if (textForAudio.length > maxLength) {
1087
1175
  if (!isSummarizationEnabled(prefsPath)) {
@@ -233,6 +233,7 @@ export async function processMessage(params) {
233
233
  MediaPath: params.msg.mediaPath,
234
234
  MediaUrl: params.msg.mediaUrl,
235
235
  MediaType: params.msg.mediaType,
236
+ MediaDownloadFailed: params.msg.mediaDownloadFailed,
236
237
  ChatType: params.msg.chatType,
237
238
  ConversationLabel: params.msg.chatType === "group" ? conversationId : params.msg.from,
238
239
  GroupSubject: params.msg.groupSubject,
@@ -230,6 +230,7 @@ export async function monitorWebInbox(options) {
230
230
  const replyContext = describeReplyContext(msg.message);
231
231
  let mediaPath;
232
232
  let mediaType;
233
+ let mediaDownloadFailed = false;
233
234
  try {
234
235
  const inboundMedia = await downloadInboundMedia(msg, sock);
235
236
  if (inboundMedia) {
@@ -241,9 +242,15 @@ export async function monitorWebInbox(options) {
241
242
  mediaPath = saved.path;
242
243
  mediaType = inboundMedia.mimetype;
243
244
  }
245
+ else if (body.includes("<media:")) {
246
+ // downloadInboundMedia returned undefined — media was expected but unavailable
247
+ mediaDownloadFailed = true;
248
+ inboundLogger.warn({ id, from, body }, "media download returned empty");
249
+ }
244
250
  }
245
251
  catch (err) {
246
- logVerbose(`Inbound media download failed: ${String(err)}`);
252
+ mediaDownloadFailed = body.includes("<media:");
253
+ inboundLogger.warn({ id, from, error: String(err) }, "media download failed");
247
254
  }
248
255
  const chatJid = remoteJid;
249
256
  const sendComposing = async () => {
@@ -294,6 +301,7 @@ export async function monitorWebInbox(options) {
294
301
  sendMedia,
295
302
  mediaPath,
296
303
  mediaType,
304
+ mediaDownloadFailed,
297
305
  };
298
306
  try {
299
307
  const task = Promise.resolve(debouncer.enqueue(inboundMessage));
@@ -15,7 +15,7 @@ else
15
15
  export NODE_PATH="/Users/neo/taskmaster-dev/dist/node_modules:/Users/neo/taskmaster-dev/node_modules:/Users/neo/node_modules:/Users/node_modules:/node_modules:/Users/neo/taskmaster-dev/node_modules/.pnpm/node_modules:$NODE_PATH"
16
16
  fi
17
17
  if [ -x "$basedir/node" ]; then
18
- exec "$basedir/node" "$basedir/../taskmaster/dist/entry.js" "$@"
18
+ exec "$basedir/node" "$basedir/../@rubytech/taskmaster/dist/entry.js" "$@"
19
19
  else
20
- exec node "$basedir/../taskmaster/dist/entry.js" "$@"
20
+ exec node "$basedir/../@rubytech/taskmaster/dist/entry.js" "$@"
21
21
  fi
@@ -31,9 +31,9 @@
31
31
  "google-auth-library": "^10.5.0"
32
32
  },
33
33
  "devDependencies": {
34
- "taskmaster": "workspace:*"
34
+ "@rubytech/taskmaster": "workspace:*"
35
35
  },
36
36
  "peerDependencies": {
37
- "taskmaster": "workspace:*"
37
+ "@rubytech/taskmaster": "workspace:*"
38
38
  }
39
39
  }
@@ -15,7 +15,7 @@ else
15
15
  export NODE_PATH="/Users/neo/taskmaster-dev/dist/node_modules:/Users/neo/taskmaster-dev/node_modules:/Users/neo/node_modules:/Users/node_modules:/node_modules:/Users/neo/taskmaster-dev/node_modules/.pnpm/node_modules:$NODE_PATH"
16
16
  fi
17
17
  if [ -x "$basedir/node" ]; then
18
- exec "$basedir/node" "$basedir/../taskmaster/dist/entry.js" "$@"
18
+ exec "$basedir/node" "$basedir/../@rubytech/taskmaster/dist/entry.js" "$@"
19
19
  else
20
- exec node "$basedir/../taskmaster/dist/entry.js" "$@"
20
+ exec node "$basedir/../@rubytech/taskmaster/dist/entry.js" "$@"
21
21
  fi
@@ -24,6 +24,6 @@
24
24
  }
25
25
  },
26
26
  "devDependencies": {
27
- "taskmaster": "workspace:*"
27
+ "@rubytech/taskmaster": "workspace:*"
28
28
  }
29
29
  }
@@ -15,7 +15,7 @@ else
15
15
  export NODE_PATH="/Users/neo/taskmaster-dev/dist/node_modules:/Users/neo/taskmaster-dev/node_modules:/Users/neo/node_modules:/Users/node_modules:/node_modules:/Users/neo/taskmaster-dev/node_modules/.pnpm/node_modules:$NODE_PATH"
16
16
  fi
17
17
  if [ -x "$basedir/node" ]; then
18
- exec "$basedir/node" "$basedir/../taskmaster/dist/entry.js" "$@"
18
+ exec "$basedir/node" "$basedir/../@rubytech/taskmaster/dist/entry.js" "$@"
19
19
  else
20
- exec node "$basedir/../taskmaster/dist/entry.js" "$@"
20
+ exec node "$basedir/../@rubytech/taskmaster/dist/entry.js" "$@"
21
21
  fi
@@ -31,6 +31,6 @@
31
31
  "zod": "^4.3.6"
32
32
  },
33
33
  "devDependencies": {
34
- "taskmaster": "workspace:*"
34
+ "@rubytech/taskmaster": "workspace:*"
35
35
  }
36
36
  }
@@ -15,7 +15,7 @@ else
15
15
  export NODE_PATH="/Users/neo/taskmaster-dev/dist/node_modules:/Users/neo/taskmaster-dev/node_modules:/Users/neo/node_modules:/Users/node_modules:/node_modules:/Users/neo/taskmaster-dev/node_modules/.pnpm/node_modules:$NODE_PATH"
16
16
  fi
17
17
  if [ -x "$basedir/node" ]; then
18
- exec "$basedir/node" "$basedir/../taskmaster/dist/entry.js" "$@"
18
+ exec "$basedir/node" "$basedir/../@rubytech/taskmaster/dist/entry.js" "$@"
19
19
  else
20
- exec node "$basedir/../taskmaster/dist/entry.js" "$@"
20
+ exec node "$basedir/../@rubytech/taskmaster/dist/entry.js" "$@"
21
21
  fi
@@ -29,7 +29,7 @@
29
29
  "@microsoft/agents-hosting": "^1.2.2",
30
30
  "@microsoft/agents-hosting-express": "^1.2.2",
31
31
  "@microsoft/agents-hosting-extensions-teams": "^1.2.2",
32
- "taskmaster": "workspace:*",
32
+ "@rubytech/taskmaster": "workspace:*",
33
33
  "express": "^5.2.1",
34
34
  "proper-lockfile": "^4.1.2"
35
35
  }
@@ -15,7 +15,7 @@ else
15
15
  export NODE_PATH="/Users/neo/taskmaster-dev/dist/node_modules:/Users/neo/taskmaster-dev/node_modules:/Users/neo/node_modules:/Users/node_modules:/node_modules:/Users/neo/taskmaster-dev/node_modules/.pnpm/node_modules:$NODE_PATH"
16
16
  fi
17
17
  if [ -x "$basedir/node" ]; then
18
- exec "$basedir/node" "$basedir/../taskmaster/dist/entry.js" "$@"
18
+ exec "$basedir/node" "$basedir/../@rubytech/taskmaster/dist/entry.js" "$@"
19
19
  else
20
- exec node "$basedir/../taskmaster/dist/entry.js" "$@"
20
+ exec node "$basedir/../@rubytech/taskmaster/dist/entry.js" "$@"
21
21
  fi
@@ -24,7 +24,7 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "taskmaster": "workspace:*",
27
+ "@rubytech/taskmaster": "workspace:*",
28
28
  "nostr-tools": "^2.20.0",
29
29
  "zod": "^4.3.6"
30
30
  }
@@ -15,7 +15,7 @@ else
15
15
  export NODE_PATH="/Users/neo/taskmaster-dev/dist/node_modules:/Users/neo/taskmaster-dev/node_modules:/Users/neo/node_modules:/Users/node_modules:/node_modules:/Users/neo/taskmaster-dev/node_modules/.pnpm/node_modules:$NODE_PATH"
16
16
  fi
17
17
  if [ -x "$basedir/node" ]; then
18
- exec "$basedir/node" "$basedir/../taskmaster/dist/entry.js" "$@"
18
+ exec "$basedir/node" "$basedir/../@rubytech/taskmaster/dist/entry.js" "$@"
19
19
  else
20
- exec node "$basedir/../taskmaster/dist/entry.js" "$@"
20
+ exec node "$basedir/../@rubytech/taskmaster/dist/entry.js" "$@"
21
21
  fi
@@ -27,7 +27,7 @@
27
27
  }
28
28
  },
29
29
  "dependencies": {
30
- "taskmaster": "workspace:*",
30
+ "@rubytech/taskmaster": "workspace:*",
31
31
  "undici": "7.19.0"
32
32
  }
33
33
  }
@@ -15,7 +15,7 @@ else
15
15
  export NODE_PATH="/Users/neo/taskmaster-dev/dist/node_modules:/Users/neo/taskmaster-dev/node_modules:/Users/neo/node_modules:/Users/node_modules:/node_modules:/Users/neo/taskmaster-dev/node_modules/.pnpm/node_modules:$NODE_PATH"
16
16
  fi
17
17
  if [ -x "$basedir/node" ]; then
18
- exec "$basedir/node" "$basedir/../taskmaster/dist/entry.js" "$@"
18
+ exec "$basedir/node" "$basedir/../@rubytech/taskmaster/dist/entry.js" "$@"
19
19
  else
20
- exec node "$basedir/../taskmaster/dist/entry.js" "$@"
20
+ exec node "$basedir/../@rubytech/taskmaster/dist/entry.js" "$@"
21
21
  fi
@@ -4,7 +4,7 @@
4
4
  "type": "module",
5
5
  "description": "Taskmaster Zalo Personal Account plugin via zca-cli",
6
6
  "dependencies": {
7
- "taskmaster": "workspace:*",
7
+ "@rubytech/taskmaster": "workspace:*",
8
8
  "@sinclair/typebox": "0.34.47"
9
9
  },
10
10
  "taskmaster": {