@gonzih/cc-discord 0.2.47 → 0.2.49

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.
@@ -4,7 +4,7 @@
4
4
  * v0.2.0 channels (discord-scoped):
5
5
  * cca:discord:notify:{ns} — job completion notifications → forward to Discord channel
6
6
  * cca:chat:incoming:{ns} — messages from the web UI → echo to Discord + feed to meta-agent
7
- * cca:discord:chat:outgoing:{ns} — meta-agent stdout lines (source=claude) → buffer+debounce → Discord
7
+ * cca:discord:chat:outgoing:{ns} — meta-agent stdout lines (source=claude/codex) → buffer+debounce → Discord
8
8
  *
9
9
  * All messages (Discord incoming, Claude responses) are also written to:
10
10
  * cca:discord:chat:log:{ns} — LPUSH + LTRIM 0 499 (last 500 messages)
@@ -23,7 +23,7 @@ export interface EvalReport {
23
23
  import type { CcDiscordBot } from "./bot.js";
24
24
  export interface ChatMessage {
25
25
  id: string;
26
- source: "discord" | "ui" | "claude" | "cc-tg" | "cc-discord";
26
+ source: "discord" | "ui" | "claude" | "codex" | "cc-tg" | "cc-discord";
27
27
  role: "user" | "assistant" | "tool";
28
28
  content: string;
29
29
  timestamp: string;
package/dist/notifier.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * v0.2.0 channels (discord-scoped):
5
5
  * cca:discord:notify:{ns} — job completion notifications → forward to Discord channel
6
6
  * cca:chat:incoming:{ns} — messages from the web UI → echo to Discord + feed to meta-agent
7
- * cca:discord:chat:outgoing:{ns} — meta-agent stdout lines (source=claude) → buffer+debounce → Discord
7
+ * cca:discord:chat:outgoing:{ns} — meta-agent stdout lines (source=claude/codex) → buffer+debounce → Discord
8
8
  *
9
9
  * All messages (Discord incoming, Claude responses) are also written to:
10
10
  * cca:discord:chat:log:{ns} — LPUSH + LTRIM 0 499 (last 500 messages)
@@ -270,15 +270,59 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
270
270
  log("info", `psubscribed to ${outgoingPattern}`);
271
271
  }
272
272
  });
273
+ const TOOL_SLOT = "tools";
274
+ const RESPONSE_SLOT = "response";
273
275
  const liveStates = new Map();
274
276
  function getLiveState(ns, targetChannelId) {
275
277
  let state = liveStates.get(ns);
276
278
  if (!state) {
277
- state = { text: "", toolStatus: "", targetChannelId, starting: false, liveStarted: false, finalTimer: null, pendingDone: false };
279
+ state = {
280
+ text: "",
281
+ toolLog: [],
282
+ activeTool: "",
283
+ targetChannelId,
284
+ responseStarting: false,
285
+ responseStarted: false,
286
+ toolStarting: false,
287
+ toolStarted: false,
288
+ finalTimer: null,
289
+ };
278
290
  liveStates.set(ns, state);
279
291
  }
280
292
  return state;
281
293
  }
294
+ function toolLogText(ns, state, live) {
295
+ const visible = state.toolLog.slice(0, 20);
296
+ const body = visible.length > 0 ? visible.join("\n") : "waiting for tool activity";
297
+ return `← [${ns}] tools\n${body}${live ? " ▋" : ""}`;
298
+ }
299
+ function ensureToolLogMessage(ns, deliverTo, state) {
300
+ if (state.toolStarted) {
301
+ bot.updateLiveMessage(deliverTo, toolLogText(ns, state, true), TOOL_SLOT);
302
+ return Promise.resolve();
303
+ }
304
+ if (state.toolStarting)
305
+ return Promise.resolve();
306
+ state.toolStarting = true;
307
+ return bot.startOrGetLiveMessage(deliverTo, toolLogText(ns, state, true), TOOL_SLOT).then((msg) => {
308
+ state.toolStarting = false;
309
+ if (msg) {
310
+ state.toolStarted = true;
311
+ bot.updateLiveMessage(deliverTo, toolLogText(ns, state, true), TOOL_SLOT);
312
+ }
313
+ }).catch(() => {
314
+ state.toolStarting = false;
315
+ });
316
+ }
317
+ function replaceNewestActiveTool(state, replacement) {
318
+ const idx = state.toolLog.findIndex((line) => line.startsWith("⚙️"));
319
+ if (idx >= 0) {
320
+ state.toolLog[idx] = replacement;
321
+ }
322
+ else {
323
+ state.toolLog.unshift(replacement);
324
+ }
325
+ }
282
326
  async function finalizeState(ns, state) {
283
327
  if (state.finalTimer) {
284
328
  clearTimeout(state.finalTimer);
@@ -288,10 +332,18 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
288
332
  const deliverTo = bot.getLoopThreadId(state.targetChannelId) ?? state.targetChannelId;
289
333
  bot.stopMetaAgentTyping(deliverTo);
290
334
  const trimmed = state.text.trim();
291
- if (!trimmed && !state.liveStarted)
335
+ const toolText = state.toolLog.length > 0 ? toolLogText(ns, state, false) : "";
336
+ if (state.toolStarted || state.toolLog.length > 0) {
337
+ await bot.finalizeLiveMessage(deliverTo, toolText, TOOL_SLOT);
338
+ }
339
+ if (!trimmed && !state.responseStarted)
292
340
  return;
293
341
  const fullText = trimmed ? `← [${ns}]\n${stripAnsi(trimmed)}` : "";
294
- await bot.finalizeLiveMessage(deliverTo, fullText);
342
+ if (fullText.length > 0 && fullText.length < 30) {
343
+ await bot.finalizeLiveMessage(deliverTo, "", RESPONSE_SLOT);
344
+ return;
345
+ }
346
+ await bot.finalizeLiveMessage(deliverTo, fullText, RESPONSE_SLOT);
295
347
  }
296
348
  function scheduleFinal(ns, state) {
297
349
  if (state.finalTimer)
@@ -312,7 +364,7 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
312
364
  catch {
313
365
  return;
314
366
  }
315
- if (parsed?.source !== "claude")
367
+ if (parsed?.source !== "claude" && parsed?.source !== "codex")
316
368
  return;
317
369
  const targetChannelId = routedChannelIds.get(ns) ??
318
370
  (ns === namespace ? (notifyChannelId ?? getActiveChannelId?.()) : undefined);
@@ -326,41 +378,29 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
326
378
  // tool_start: show activity overlay, suspend finalize timer
327
379
  if (event === "tool_start") {
328
380
  const toolName = parsed.content || "tool";
329
- state.toolStatus = toolName;
381
+ state.activeTool = toolName;
382
+ state.toolLog.unshift(`⚙️ \`${toolName}\`...`);
330
383
  if (state.finalTimer) {
331
384
  clearTimeout(state.finalTimer);
332
385
  state.finalTimer = null;
333
386
  }
334
- if (state.liveStarted) {
335
- const textPart = state.text.trim() ? `← [${ns}]\n${stripAnsi(state.text.trim())}\n\n` : "";
336
- bot.updateLiveMessage(deliverTo, `${textPart}⚙️ \`${toolName}\`...`);
337
- }
338
- else if (!state.starting) {
339
- state.starting = true;
340
- bot.startOrGetLiveMessage(deliverTo, `⚙️ \`${toolName}\`...`).then((msg) => {
341
- state.starting = false;
342
- if (msg)
343
- state.liveStarted = true;
344
- }).catch(() => { state.starting = false; });
345
- }
387
+ void ensureToolLogMessage(ns, deliverTo, state);
346
388
  return;
347
389
  }
348
390
  // tool_end: clear overlay, resume finalize timer if text is pending
349
391
  if (event === "tool_end") {
350
- state.toolStatus = "";
392
+ const toolName = state.activeTool || "tool";
393
+ replaceNewestActiveTool(state, `✓ \`${toolName}\``);
394
+ state.activeTool = "";
395
+ if (state.toolStarted) {
396
+ bot.updateLiveMessage(deliverTo, toolLogText(ns, state, true), TOOL_SLOT);
397
+ }
351
398
  if (state.text.trim())
352
399
  scheduleFinal(ns, state);
353
400
  return;
354
401
  }
355
402
  // done: immediate finalization (fired by meta-agent-manager after result event)
356
403
  if (event === "done") {
357
- if (state.starting) {
358
- // startOrGetLiveMessage is still in flight — defer finalization to its .then() callback
359
- // so we don't race: finalizeState runs before the Discord message exists → fallback
360
- // sends a second plain message, leaving the live message orphaned with ▋.
361
- state.pendingDone = true;
362
- return;
363
- }
364
404
  finalizeState(ns, state).catch((err) => {
365
405
  log("warn", `meta-agent done finalize failed (ns=${ns}):`, err.message);
366
406
  });
@@ -370,31 +410,27 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
370
410
  const content = parsed.content;
371
411
  if (!content)
372
412
  return;
373
- state.toolStatus = ""; // text arrived clear any tool overlay
374
- state.text += (state.text ? "\n" : "") + content;
413
+ state.text += parsed.source === "codex"
414
+ ? content
415
+ : (state.text ? "\n" : "") + content;
375
416
  const textDisplay = `← [${ns}]\n${stripAnsi(state.text)} ▋`;
376
- if (!state.liveStarted) {
377
- if (!state.starting) {
378
- state.starting = true;
379
- // startOrGetLiveMessage reuses existing message if tool_start created one
380
- bot.startOrGetLiveMessage(deliverTo, textDisplay).then((msg) => {
381
- state.starting = false;
417
+ if (!state.responseStarted) {
418
+ if (!state.responseStarting) {
419
+ state.responseStarting = true;
420
+ ensureToolLogMessage(ns, deliverTo, state).then(() => {
421
+ return bot.startOrGetLiveMessage(deliverTo, textDisplay, RESPONSE_SLOT);
422
+ }).then((msg) => {
423
+ state.responseStarting = false;
382
424
  if (msg) {
383
- state.liveStarted = true;
384
- bot.updateLiveMessage(deliverTo, textDisplay);
385
- }
386
- // done arrived while we were creating the message — finalize now
387
- if (state.pendingDone) {
388
- finalizeState(ns, state).catch((err) => {
389
- log("warn", `meta-agent deferred done finalize failed (ns=${ns}):`, err.message);
390
- });
425
+ state.responseStarted = true;
426
+ bot.updateLiveMessage(deliverTo, textDisplay, RESPONSE_SLOT);
391
427
  }
392
- }).catch(() => { state.starting = false; });
428
+ }).catch(() => { state.responseStarting = false; });
393
429
  }
394
430
  // Text is buffered in state.text; applied when startOrGetLiveMessage resolves
395
431
  }
396
432
  else {
397
- bot.updateLiveMessage(deliverTo, textDisplay);
433
+ bot.updateLiveMessage(deliverTo, textDisplay, RESPONSE_SLOT);
398
434
  }
399
435
  scheduleFinal(ns, state);
400
436
  });
package/dist/tokens.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Supports CLAUDE_CODE_OAUTH_TOKENS (comma-separated list of tokens).
5
5
  * Falls back to CLAUDE_CODE_OAUTH_TOKEN for single-token / backwards compat.
6
6
  *
7
- * cc-tg token pool rotates independently from cc-agent's pool. No coordination between them.
7
+ * cc-tg token pool rotates independently from cc-discord's local session pool.
8
8
  */
9
9
  /**
10
10
  * Load tokens from env vars. Called on startup; also re-callable in tests.
package/dist/tokens.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Supports CLAUDE_CODE_OAUTH_TOKENS (comma-separated list of tokens).
5
5
  * Falls back to CLAUDE_CODE_OAUTH_TOKEN for single-token / backwards compat.
6
6
  *
7
- * cc-tg token pool rotates independently from cc-agent's pool. No coordination between them.
7
+ * cc-tg token pool rotates independently from cc-discord's local session pool.
8
8
  */
9
9
  let tokens = [];
10
10
  let currentIndex = 0;
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@gonzih/cc-discord",
3
- "version": "0.2.47",
4
- "description": "Claude Code Discord bot chat with Claude Code via Discord",
3
+ "version": "0.2.49",
4
+ "description": "Discord bot for routed Claude Code or Codex agents with ATC dispatch",
5
5
  "type": "module",
6
6
  "bin": {
7
- "cc-discord": "./dist/index.js"
7
+ "cc-discord": "dist/index.js"
8
8
  },
9
9
  "scripts": {
10
10
  "build": "tsc && chmod +x dist/index.js",
@@ -13,7 +13,8 @@
13
13
  "test": "vitest run",
14
14
  "test:watch": "vitest",
15
15
  "test:coverage": "vitest run --coverage",
16
- "test:integration": "CLAUDE_BIN=./test/fixtures/mock-claude.js vitest run test/integration/"
16
+ "test:integration": "CLAUDE_BIN=./test/fixtures/mock-claude.js vitest run test/integration/",
17
+ "test:integration:codex": "CC_DISCORD_AGENT_DRIVER=codex CODEX_BIN=./test/fixtures/mock-codex.js vitest run test/integration/meta-agent.test.ts"
17
18
  },
18
19
  "files": [
19
20
  "dist/"
@@ -33,7 +34,7 @@
33
34
  },
34
35
  "repository": {
35
36
  "type": "git",
36
- "url": "https://github.com/Gonzih/cc-discord.git"
37
+ "url": "git+https://github.com/Gonzih/cc-discord.git"
37
38
  },
38
39
  "homepage": "https://github.com/Gonzih/cc-discord",
39
40
  "bugs": {
@@ -42,6 +43,8 @@
42
43
  "keywords": [
43
44
  "claude",
44
45
  "claude-code",
46
+ "codex",
47
+ "atc",
45
48
  "discord",
46
49
  "bot",
47
50
  "ai"