@gonzih/cc-discord 0.2.49 → 0.2.50

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
@@ -413,7 +413,7 @@ export class CcDiscordBot {
413
413
  await this.editLiveMessageWithPacing(key, entry.msg, entry.text, 1);
414
414
  }
415
415
  catch (err) {
416
- if (this.liveMessages.get(key)?.msg.id === entry.msg.id) {
416
+ if (!this.liveEditQueue.has(key) && this.liveMessages.get(key)?.msg.id === entry.msg.id) {
417
417
  this.liveEditQueue.set(key, entry);
418
418
  }
419
419
  }
package/dist/notifier.js CHANGED
@@ -272,6 +272,7 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
272
272
  });
273
273
  const TOOL_SLOT = "tools";
274
274
  const RESPONSE_SLOT = "response";
275
+ const showToolUsage = () => /^(1|true|yes)$/i.test(process.env.CC_DISCORD_SHOW_TOOL_USAGE ?? "");
275
276
  const liveStates = new Map();
276
277
  function getLiveState(ns, targetChannelId) {
277
278
  let state = liveStates.get(ns);
@@ -333,7 +334,7 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
333
334
  bot.stopMetaAgentTyping(deliverTo);
334
335
  const trimmed = state.text.trim();
335
336
  const toolText = state.toolLog.length > 0 ? toolLogText(ns, state, false) : "";
336
- if (state.toolStarted || state.toolLog.length > 0) {
337
+ if (showToolUsage() && (state.toolStarted || state.toolLog.length > 0)) {
337
338
  await bot.finalizeLiveMessage(deliverTo, toolText, TOOL_SLOT);
338
339
  }
339
340
  if (!trimmed && !state.responseStarted)
@@ -379,20 +380,24 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
379
380
  if (event === "tool_start") {
380
381
  const toolName = parsed.content || "tool";
381
382
  state.activeTool = toolName;
382
- state.toolLog.unshift(`⚙️ \`${toolName}\`...`);
383
383
  if (state.finalTimer) {
384
384
  clearTimeout(state.finalTimer);
385
385
  state.finalTimer = null;
386
386
  }
387
- void ensureToolLogMessage(ns, deliverTo, state);
387
+ if (showToolUsage()) {
388
+ state.toolLog.unshift(`⚙️ \`${toolName}\`...`);
389
+ void ensureToolLogMessage(ns, deliverTo, state);
390
+ }
388
391
  return;
389
392
  }
390
393
  // tool_end: clear overlay, resume finalize timer if text is pending
391
394
  if (event === "tool_end") {
392
395
  const toolName = state.activeTool || "tool";
393
- replaceNewestActiveTool(state, `✓ \`${toolName}\``);
394
396
  state.activeTool = "";
395
- if (state.toolStarted) {
397
+ if (showToolUsage()) {
398
+ replaceNewestActiveTool(state, `✓ \`${toolName}\``);
399
+ }
400
+ if (showToolUsage() && state.toolStarted) {
396
401
  bot.updateLiveMessage(deliverTo, toolLogText(ns, state, true), TOOL_SLOT);
397
402
  }
398
403
  if (state.text.trim())
@@ -417,7 +422,10 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
417
422
  if (!state.responseStarted) {
418
423
  if (!state.responseStarting) {
419
424
  state.responseStarting = true;
420
- ensureToolLogMessage(ns, deliverTo, state).then(() => {
425
+ const beforeResponse = showToolUsage()
426
+ ? ensureToolLogMessage(ns, deliverTo, state)
427
+ : Promise.resolve();
428
+ beforeResponse.then(() => {
421
429
  return bot.startOrGetLiveMessage(deliverTo, textDisplay, RESPONSE_SLOT);
422
430
  }).then((msg) => {
423
431
  state.responseStarting = false;
package/dist/voice.js CHANGED
@@ -4,22 +4,36 @@
4
4
  */
5
5
  import { execFile } from "child_process";
6
6
  import { promisify } from "util";
7
- import { existsSync, createWriteStream } from "fs";
8
- import { unlink, readFile, access } from "fs/promises";
7
+ import { existsSync, createWriteStream, readdirSync } from "fs";
8
+ import { unlink, readFile, access, mkdir } 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
13
  const execFileAsync = promisify(execFile);
14
- // Env var overrides allow test injection: WHISPER_BIN, FFMPEG_BIN, WHISPER_MODEL
14
+ // Env var overrides allow test injection: WHISPER_BIN, FFMPEG_BIN, WHISPER_MODEL.
15
+ // WHISPER_MODEL may point to either a model file or a directory containing ggml*.bin.
16
+ const WHISPER_MODEL_NAMES = [
17
+ "ggml-small.en.bin",
18
+ "ggml-small.bin",
19
+ "ggml-base.en.bin",
20
+ "ggml-base.bin",
21
+ "ggml-tiny.en.bin",
22
+ "ggml-tiny.bin",
23
+ ];
15
24
  const WHISPER_MODELS = [
16
25
  process.env.WHISPER_MODEL,
26
+ process.env.WHISPER_MODEL_DIR,
17
27
  "/opt/homebrew/share/whisper-cpp/ggml-small.en.bin",
18
28
  "/opt/homebrew/share/whisper-cpp/ggml-small.bin",
19
29
  "/opt/homebrew/share/whisper-cpp/ggml-base.en.bin",
20
30
  "/opt/homebrew/share/whisper-cpp/ggml-base.bin",
31
+ "/opt/homebrew/share/whisper-cpp",
32
+ "/usr/local/share/whisper-cpp",
21
33
  `${process.env.HOME}/.local/share/whisper-cpp/ggml-small.en.bin`,
22
34
  `${process.env.HOME}/.local/share/whisper-cpp/ggml-base.en.bin`,
35
+ `${process.env.HOME}/.local/share/whisper-cpp`,
36
+ `${process.env.HOME}/Library/Application Support/whisper-cpp`,
23
37
  ].filter(Boolean);
24
38
  const WHISPER_BIN_CANDIDATES = [
25
39
  process.env.WHISPER_BIN,
@@ -35,6 +49,11 @@ const FFMPEG_CANDIDATES = [
35
49
  "/usr/local/bin/ffmpeg",
36
50
  "/usr/bin/ffmpeg",
37
51
  ].filter(Boolean);
52
+ const WHISPER_DOWNLOAD_CANDIDATES = [
53
+ process.env.WHISPER_DOWNLOAD_BIN,
54
+ "/opt/homebrew/bin/whisper-cpp-download-ggml-model",
55
+ "/usr/local/bin/whisper-cpp-download-ggml-model",
56
+ ].filter(Boolean);
38
57
  function findBin(candidates) {
39
58
  for (const p of candidates) {
40
59
  if (existsSync(p))
@@ -44,11 +63,39 @@ function findBin(candidates) {
44
63
  }
45
64
  function findModel() {
46
65
  for (const p of WHISPER_MODELS) {
47
- if (existsSync(p))
66
+ if (!existsSync(p))
67
+ continue;
68
+ try {
69
+ const entries = readdirSync(p, { withFileTypes: true });
70
+ const model = WHISPER_MODEL_NAMES.find((name) => entries.some((entry) => entry.isFile() && entry.name === name));
71
+ if (model)
72
+ return join(p, model);
73
+ const anyModel = entries.find((entry) => entry.isFile() && /^ggml.*\.bin$/i.test(entry.name));
74
+ if (anyModel)
75
+ return join(p, anyModel.name);
76
+ }
77
+ catch {
48
78
  return p;
79
+ }
49
80
  }
50
81
  return null;
51
82
  }
83
+ async function ensureModel() {
84
+ const existing = findModel();
85
+ if (existing)
86
+ return existing;
87
+ const downloader = findBin(WHISPER_DOWNLOAD_CANDIDATES);
88
+ if (!downloader)
89
+ return null;
90
+ const modelDir = process.env.WHISPER_MODEL_DIR || `${process.env.HOME}/.local/share/whisper-cpp`;
91
+ await mkdir(modelDir, { recursive: true });
92
+ await execFileAsync(downloader, ["small.en"], {
93
+ cwd: modelDir,
94
+ timeout: 10 * 60_000,
95
+ maxBuffer: 1024 * 1024,
96
+ });
97
+ return findModel();
98
+ }
52
99
  /**
53
100
  * Download a file from a URL to dest.
54
101
  * Supports file:// (copies local file), and https/http with redirect following.
@@ -95,7 +142,7 @@ export async function transcribeVoice(fileUrl) {
95
142
  const ffmpegBin = findBin(FFMPEG_CANDIDATES);
96
143
  if (!ffmpegBin)
97
144
  throw new Error("ffmpeg not found — install with: brew install ffmpeg");
98
- const model = findModel();
145
+ const model = await ensureModel();
99
146
  if (!model)
100
147
  throw new Error("No whisper model found — run: whisper-cpp-download-ggml-model small.en");
101
148
  const tmp = join(tmpdir(), `cc-discord-voice-${Date.now()}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-discord",
3
- "version": "0.2.49",
3
+ "version": "0.2.50",
4
4
  "description": "Discord bot for routed Claude Code or Codex agents with ATC dispatch",
5
5
  "type": "module",
6
6
  "bin": {