@gonzih/cc-discord 0.2.38 → 0.2.39

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/dist/bot.js CHANGED
@@ -911,8 +911,15 @@ export class CcDiscordBot {
911
911
  this.lastActiveChannelId = channelId;
912
912
  switch (interaction.commandName) {
913
913
  case "reset": {
914
+ // Kill local DM session if any
914
915
  this.killSession(channelId);
915
- await interaction.reply("Session reset. Send a message to start.");
916
+ // Kill persistent meta-agent session if this is a routed channel.
917
+ // Unlike /clear, we do NOT delete the JSONL — context is preserved.
918
+ // Next message will respawn with --continue, picking up the same history.
919
+ const resetNs = this.resolveNamespaceForChannel(channelId);
920
+ this.metaAgentManager.killSession(resetNs);
921
+ await interaction.reply(`Session process killed for \`${resetNs}\`. Next message respawns with existing context intact.\n` +
922
+ `Use \`/clear\` to also wipe conversation history.`);
916
923
  break;
917
924
  }
918
925
  case "costs": {
package/dist/voice.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Voice message transcription via whisper.cpp.
3
- * Flow: Telegram OGG → ffmpeg convert to 16kHz WAV → whisper-cpp → text
3
+ * Flow: Discord voice memo (OGG/M4A) → ffmpeg convert to 16kHz WAV → whisper-cpp → text
4
4
  */
5
5
  /**
6
- * Transcribe a voice message from a Telegram file URL.
6
+ * Transcribe a voice message from a Discord CDN URL.
7
+ * Accepts OGG, M4A, MP3, or any audio format ffmpeg can decode.
7
8
  * Returns the transcribed text, or throws if whisper/ffmpeg not available.
8
9
  */
9
10
  export declare function transcribeVoice(fileUrl: string): Promise<string>;
package/dist/voice.js CHANGED
@@ -1,40 +1,40 @@
1
1
  /**
2
2
  * Voice message transcription via whisper.cpp.
3
- * Flow: Telegram OGG → ffmpeg convert to 16kHz WAV → whisper-cpp → text
3
+ * Flow: Discord voice memo (OGG/M4A) → ffmpeg convert to 16kHz WAV → whisper-cpp → text
4
4
  */
5
5
  import { execFile } from "child_process";
6
6
  import { promisify } from "util";
7
- import { existsSync } from "fs";
8
- import { unlink, readFile } from "fs/promises";
7
+ import { existsSync, createWriteStream } from "fs";
8
+ import { unlink, readFile, access } from "fs/promises";
9
9
  import { tmpdir } from "os";
10
10
  import { join } from "path";
11
11
  import https from "https";
12
12
  import http from "http";
13
- import { createWriteStream } from "fs";
14
13
  const execFileAsync = promisify(execFile);
15
- // Whisper model small.en is fast and accurate enough for commands
16
- // Falls back to base.en if small not found
14
+ // Env var overrides allow test injection: WHISPER_BIN, FFMPEG_BIN, WHISPER_MODEL
17
15
  const WHISPER_MODELS = [
16
+ process.env.WHISPER_MODEL,
18
17
  "/opt/homebrew/share/whisper-cpp/ggml-small.en.bin",
19
18
  "/opt/homebrew/share/whisper-cpp/ggml-small.bin",
20
19
  "/opt/homebrew/share/whisper-cpp/ggml-base.en.bin",
21
20
  "/opt/homebrew/share/whisper-cpp/ggml-base.bin",
22
- // user-local
23
21
  `${process.env.HOME}/.local/share/whisper-cpp/ggml-small.en.bin`,
24
22
  `${process.env.HOME}/.local/share/whisper-cpp/ggml-base.en.bin`,
25
- ];
23
+ ].filter(Boolean);
26
24
  const WHISPER_BIN_CANDIDATES = [
27
- "/opt/homebrew/bin/whisper-cli", // whisper-cpp brew formula installs as whisper-cli
25
+ process.env.WHISPER_BIN,
26
+ "/opt/homebrew/bin/whisper-cli",
28
27
  "/opt/homebrew/bin/whisper-cpp",
29
28
  "/usr/local/bin/whisper-cli",
30
29
  "/usr/local/bin/whisper-cpp",
31
30
  "/opt/homebrew/bin/whisper",
32
- ];
31
+ ].filter(Boolean);
33
32
  const FFMPEG_CANDIDATES = [
33
+ process.env.FFMPEG_BIN,
34
34
  "/opt/homebrew/bin/ffmpeg",
35
35
  "/usr/local/bin/ffmpeg",
36
36
  "/usr/bin/ffmpeg",
37
- ];
37
+ ].filter(Boolean);
38
38
  function findBin(candidates) {
39
39
  for (const p of candidates) {
40
40
  if (existsSync(p))
@@ -49,15 +49,34 @@ function findModel() {
49
49
  }
50
50
  return null;
51
51
  }
52
- function downloadFile(url, dest) {
52
+ /**
53
+ * Download a file from a URL to dest.
54
+ * Supports file:// (copies local file), and https/http with redirect following.
55
+ */
56
+ async function downloadFile(url, dest, redirectsLeft = 5) {
57
+ if (url.startsWith("file://")) {
58
+ const { copyFile } = await import("fs/promises");
59
+ await copyFile(url.slice("file://".length), dest);
60
+ return;
61
+ }
53
62
  return new Promise((resolve, reject) => {
54
- const file = createWriteStream(dest);
63
+ if (redirectsLeft <= 0) {
64
+ reject(new Error(`Too many redirects downloading ${url}`));
65
+ return;
66
+ }
55
67
  const getter = url.startsWith("https") ? https : http;
56
68
  getter.get(url, (res) => {
69
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
70
+ res.resume();
71
+ downloadFile(res.headers.location, dest, redirectsLeft - 1).then(resolve).catch(reject);
72
+ return;
73
+ }
57
74
  if (res.statusCode !== 200) {
75
+ res.resume();
58
76
  reject(new Error(`HTTP ${res.statusCode} downloading ${url}`));
59
77
  return;
60
78
  }
79
+ const file = createWriteStream(dest);
61
80
  res.pipe(file);
62
81
  file.on("finish", () => file.close(() => resolve()));
63
82
  file.on("error", reject);
@@ -65,7 +84,8 @@ function downloadFile(url, dest) {
65
84
  });
66
85
  }
67
86
  /**
68
- * Transcribe a voice message from a Telegram file URL.
87
+ * Transcribe a voice message from a Discord CDN URL.
88
+ * Accepts OGG, M4A, MP3, or any audio format ffmpeg can decode.
69
89
  * Returns the transcribed text, or throws if whisper/ffmpeg not available.
70
90
  */
71
91
  export async function transcribeVoice(fileUrl) {
@@ -78,58 +98,82 @@ export async function transcribeVoice(fileUrl) {
78
98
  const model = findModel();
79
99
  if (!model)
80
100
  throw new Error("No whisper model found — run: whisper-cpp-download-ggml-model small.en");
81
- const tmp = join(tmpdir(), `cc-tg-voice-${Date.now()}`);
82
- const oggPath = `${tmp}.ogg`;
101
+ const tmp = join(tmpdir(), `cc-discord-voice-${Date.now()}`);
102
+ // Preserve original extension so ffmpeg auto-detects format
103
+ const urlPath = new URL(fileUrl).pathname;
104
+ const ext = urlPath.includes(".") ? "." + urlPath.split(".").pop().split("?")[0] : ".ogg";
105
+ const audioPath = `${tmp}${ext}`;
83
106
  const wavPath = `${tmp}.wav`;
84
107
  try {
85
- // 1. Download OGG from Telegram
86
- await downloadFile(fileUrl, oggPath);
87
- // 2. Convert OGG 16kHz mono WAV (whisper requirement)
88
- await execFileAsync(ffmpegBin, [
89
- "-y", "-i", oggPath,
90
- "-ar", "16000",
91
- "-ac", "1",
92
- "-c:a", "pcm_s16le",
93
- wavPath,
94
- ]);
108
+ // 1. Download audio from Discord CDN (follows redirects)
109
+ await downloadFile(fileUrl, audioPath);
110
+ // 2. Convert to 16kHz mono WAV (whisper requirement)
111
+ try {
112
+ await execFileAsync(ffmpegBin, [
113
+ "-y", "-i", audioPath,
114
+ "-ar", "16000",
115
+ "-ac", "1",
116
+ "-c:a", "pcm_s16le",
117
+ wavPath,
118
+ ]);
119
+ }
120
+ catch (err) {
121
+ const msg = err instanceof Error ? err.message : String(err);
122
+ throw new Error(`ffmpeg conversion failed: ${msg}`);
123
+ }
95
124
  // 3. Run whisper-cpp
96
- // --output-txt writes to ${wavPath}.txt (NOT stdout)
97
- // -l auto fails with .en models — detect and use -l en instead
125
+ // -l auto fails with .en models — use -l en for those
98
126
  const isEnModel = model.includes(".en.");
99
127
  const langArgs = isEnModel ? ["-l", "en"] : ["-l", "auto"];
128
+ let whisperStderr = "";
100
129
  try {
101
- await execFileAsync(whisperBin, [
130
+ const result = await execFileAsync(whisperBin, [
102
131
  "-m", model,
103
132
  "-f", wavPath,
104
133
  "--no-timestamps",
105
134
  ...langArgs,
106
- "--output-txt", // writes to wavPath + ".txt"
135
+ "--output-txt",
107
136
  ]);
137
+ whisperStderr = result.stderr ?? "";
108
138
  }
109
139
  catch (err) {
110
- const msg = err instanceof Error ? err.message : String(err);
111
- throw new Error(`whisper-cpp failed: ${msg}`);
140
+ const e = err;
141
+ whisperStderr = e.stderr ?? "";
142
+ const msg = e.message ?? String(err);
143
+ throw new Error(`whisper-cpp failed: ${msg}\nstderr: ${whisperStderr}`);
112
144
  }
113
- // Read the output file whisper-cpp wrote
114
- const txtPath = `${wavPath}.txt`;
145
+ // 4. Find output file whisper-cpp versions differ on suffix:
146
+ // some write {wavPath}.txt, others write {baseName}.txt (strip .wav)
147
+ const txtPathFull = `${wavPath}.txt`; // /tmp/foo.wav.txt
148
+ const txtPathStripped = wavPath.replace(/\.wav$/, ".txt"); // /tmp/foo.txt
115
149
  let raw = "";
116
- try {
117
- raw = await readFile(txtPath, "utf-8");
150
+ for (const candidate of [txtPathFull, txtPathStripped]) {
151
+ try {
152
+ await access(candidate);
153
+ raw = await readFile(candidate, "utf-8");
154
+ await unlink(candidate).catch(() => { });
155
+ break;
156
+ }
157
+ catch {
158
+ // try next
159
+ }
160
+ }
161
+ if (!raw && !whisperStderr) {
162
+ throw new Error("whisper-cpp ran but produced no output file (tried .wav.txt and .txt)");
118
163
  }
119
- catch {
120
- throw new Error("whisper-cpp ran but produced no output file");
164
+ // Use stdout text if file wasn't found but whisper printed to stderr (some versions do)
165
+ if (!raw) {
166
+ raw = whisperStderr;
121
167
  }
122
168
  const text = raw
123
169
  .replace(/\[BLANK_AUDIO\]/gi, "")
124
- .replace(/\[.*?\]/g, "") // remove timestamp artifacts
170
+ .replace(/\[.*?\]/g, "")
125
171
  .trim();
126
172
  return text || "[empty transcription]";
127
173
  }
128
174
  finally {
129
- // Cleanup temp files
130
- await unlink(oggPath).catch(() => { });
175
+ await unlink(audioPath).catch(() => { });
131
176
  await unlink(wavPath).catch(() => { });
132
- await unlink(`${wavPath}.txt`).catch(() => { });
133
177
  }
134
178
  }
135
179
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-discord",
3
- "version": "0.2.38",
3
+ "version": "0.2.39",
4
4
  "description": "Claude Code Discord bot — chat with Claude Code via Discord",
5
5
  "type": "module",
6
6
  "bin": {