@gonzih/cc-discord 0.1.3 → 0.1.5

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.d.ts CHANGED
@@ -36,7 +36,7 @@ export declare class CcDiscordBot {
36
36
  /** Channels created by the bot for a meta-agent namespace → skip local Claude session */
37
37
  private channelNamespaceMap;
38
38
  private storeSnowflake;
39
- private reverseSnowflakeLookup;
39
+ reverseSnowflakeLookup(n: number): string | undefined;
40
40
  /** Session key: "channelId" or "channelId:threadId" for threads */
41
41
  private sessionKey;
42
42
  /** Get the channel/thread for sending messages */
package/dist/bot.js CHANGED
@@ -82,6 +82,16 @@ async function fetchAsBase64(url) {
82
82
  }).on("error", reject);
83
83
  });
84
84
  }
85
+ /**
86
+ * Resolve the Discord category ID to use as `parent` when creating new text channels.
87
+ * Priority: DISCORD_DEFAULT_CATEGORY_ID env var → first category named "Text Channels" → undefined.
88
+ */
89
+ function resolveCategoryId(guild) {
90
+ if (process.env.DISCORD_DEFAULT_CATEGORY_ID)
91
+ return process.env.DISCORD_DEFAULT_CATEGORY_ID;
92
+ const category = guild.channels.cache.find((ch) => ch.type === ChannelType.GuildCategory && /text channels/i.test(ch.name));
93
+ return category?.id;
94
+ }
85
95
  export class CcDiscordBot {
86
96
  client;
87
97
  sessions = new Map();
@@ -120,6 +130,12 @@ export class CcDiscordBot {
120
130
  this.registerSlashCommands().catch((err) => {
121
131
  console.error("[discord] slash command registration failed:", err.message);
122
132
  });
133
+ // Pre-populate snowflakeMap so reverse-lookup works for all channels visible at login
134
+ for (const [, guild] of readyClient.guilds.cache) {
135
+ for (const [, channel] of guild.channels.cache) {
136
+ this.storeSnowflake(channel.id);
137
+ }
138
+ }
123
139
  });
124
140
  this.client.on(Events.MessageCreate, (msg) => {
125
141
  void this.handleMessage(msg);
@@ -618,7 +634,7 @@ export class CcDiscordBot {
618
634
  }
619
635
  await interaction.deferReply();
620
636
  try {
621
- const newChannel = await guild.channels.create({ name: namespace, type: ChannelType.GuildText });
637
+ const newChannel = await guild.channels.create({ name: namespace, type: ChannelType.GuildText, parent: resolveCategoryId(guild) });
622
638
  this.channelNamespaceMap.set(newChannel.id, { namespace, repoUrl });
623
639
  this.opts.registerRoutedChannelId?.(namespace, newChannel.id);
624
640
  await interaction.editReply(`Created <#${newChannel.id}> — messages there route to the ${repoUrl} meta-agent`);
@@ -648,7 +664,11 @@ export class CcDiscordBot {
648
664
  await interaction.reply("No cron jobs for this channel.");
649
665
  }
650
666
  else {
651
- const lines = jobs.map((j) => `• **${j.id}** ${j.schedule}: \`${j.prompt}\``);
667
+ const lines = jobs.map((j) => {
668
+ const chanId = this.reverseSnowflakeLookup(j.chatId);
669
+ const chanMention = chanId ? ` <#${chanId}>` : "";
670
+ return `• **${j.id}**${chanMention} ${j.schedule}: \`${j.prompt}\``;
671
+ });
652
672
  await interaction.reply(lines.join("\n"));
653
673
  }
654
674
  break;
@@ -752,7 +772,7 @@ export class CcDiscordBot {
752
772
  }
753
773
  let newChannel;
754
774
  try {
755
- newChannel = await guild.channels.create({ name: namespace, type: ChannelType.GuildText });
775
+ newChannel = await guild.channels.create({ name: namespace, type: ChannelType.GuildText, parent: resolveCategoryId(guild) });
756
776
  }
757
777
  catch (err) {
758
778
  await channel.send(`Failed to create channel: ${err.message}`).catch(() => { });
package/dist/index.js CHANGED
@@ -90,7 +90,7 @@ const bot = new CcDiscordBot({
90
90
  namespace,
91
91
  registerRoutedChannelId: (ns, channelId) => notifier.registerRoutedChannelId(ns, channelId),
92
92
  });
93
- const notifier = startNotifier(bot, notifyChannelId, namespace, sharedRedis, (channelId, text) => handleUserMessageFn?.(channelId, text), (channelId, text) => forwardNotificationFn?.(channelId, text), () => getLastActiveChannelIdFn());
93
+ const notifier = startNotifier(bot, notifyChannelId, namespace, sharedRedis, (channelId, text) => handleUserMessageFn?.(channelId, text), (channelId, text) => forwardNotificationFn?.(channelId, text), () => getLastActiveChannelIdFn(), (n) => bot.reverseSnowflakeLookup(n));
94
94
  console.log(`[notifier] started for namespace=${namespace} notifyChannelId=${notifyChannelId ?? "dynamic"}`);
95
95
  // Wire closures now that bot is constructed
96
96
  getLastActiveChannelIdFn = () => bot.getLastActiveChannelId();
@@ -20,12 +20,17 @@ export interface ChatMessage {
20
20
  timestamp: string;
21
21
  chatId: number;
22
22
  }
23
+ export interface ParsedNotification {
24
+ text: string;
25
+ chatId?: number;
26
+ }
23
27
  /**
24
- * Parse a notification payload and return the display text.
25
- * Appends a [driver] or [driver:model] badge whenever the driver field is present.
28
+ * Parse a notification payload.
29
+ * Returns the display text plus an optional chatId for per-channel routing.
30
+ * Appends a [driver] or [driver:model] badge when present.
26
31
  * Appends " cost: $X.XXX" if a numeric cost field is present.
27
32
  */
28
- export declare function parseNotification(raw: string): string;
33
+ export declare function parseNotification(raw: string): ParsedNotification;
29
34
  /**
30
35
  * Write a message to the chat log in Redis.
31
36
  * Fire-and-forget — errors are logged but not thrown.
@@ -42,12 +47,13 @@ export interface NotifierHandle {
42
47
  /**
43
48
  * Start the Discord notifier.
44
49
  *
45
- * @param bot - CcDiscordBot instance (for sending messages)
46
- * @param notifyChannelId - Discord channel ID to forward notifications to. Pass null to use getActiveChannelId.
47
- * @param namespace - cc-agent namespace (used to build Redis channel names)
48
- * @param redis - ioredis client in normal mode (will be duplicated for pub/sub)
49
- * @param handleUserMessage - Optional callback to feed UI messages into the active Claude session
50
- * @param forwardNotification - Optional callback to forward job notifications
51
- * @param getActiveChannelId - Optional callback to resolve channelId dynamically
50
+ * @param bot - CcDiscordBot instance (for sending messages)
51
+ * @param notifyChannelId - Discord channel ID to forward notifications to. Pass null to use getActiveChannelId.
52
+ * @param namespace - cc-agent namespace (used to build Redis channel names)
53
+ * @param redis - ioredis client in normal mode (will be duplicated for pub/sub)
54
+ * @param handleUserMessage - Optional callback to feed UI messages into the active Claude session
55
+ * @param forwardNotification - Optional callback to forward job notifications
56
+ * @param getActiveChannelId - Optional callback to resolve channelId dynamically
57
+ * @param reverseSnowflakeLookup - Optional callback to resolve a chatId integer to a Discord channelId
52
58
  */
53
- export declare function startNotifier(bot: CcDiscordBot, notifyChannelId: string | null, namespace: string, redis: Redis, handleUserMessage?: (channelId: string, text: string) => void, forwardNotification?: (channelId: string, text: string) => void, getActiveChannelId?: () => string | undefined): NotifierHandle;
59
+ export declare function startNotifier(bot: CcDiscordBot, notifyChannelId: string | null, namespace: string, redis: Redis, handleUserMessage?: (channelId: string, text: string) => void, forwardNotification?: (channelId: string, text: string) => void, getActiveChannelId?: () => string | undefined, reverseSnowflakeLookup?: (n: number) => string | undefined): NotifierHandle;
package/dist/notifier.js CHANGED
@@ -31,8 +31,9 @@ function shortenModelName(model, driver) {
31
31
  return model;
32
32
  }
33
33
  /**
34
- * Parse a notification payload and return the display text.
35
- * Appends a [driver] or [driver:model] badge whenever the driver field is present.
34
+ * Parse a notification payload.
35
+ * Returns the display text plus an optional chatId for per-channel routing.
36
+ * Appends a [driver] or [driver:model] badge when present.
36
37
  * Appends " cost: $X.XXX" if a numeric cost field is present.
37
38
  */
38
39
  export function parseNotification(raw) {
@@ -40,6 +41,7 @@ export function parseNotification(raw) {
40
41
  let driver;
41
42
  let model;
42
43
  let cost;
44
+ let chatId;
43
45
  try {
44
46
  const parsed = JSON.parse(raw);
45
47
  if (parsed.text)
@@ -48,16 +50,18 @@ export function parseNotification(raw) {
48
50
  model = parsed.model;
49
51
  if (typeof parsed.cost === "number")
50
52
  cost = parsed.cost;
53
+ if (typeof parsed.chat_id === "number" && parsed.chat_id !== 0)
54
+ chatId = parsed.chat_id;
51
55
  }
52
56
  catch {
53
- return text;
57
+ return { text };
54
58
  }
55
59
  if (!driver)
56
- return text;
60
+ return { text, chatId };
57
61
  const shortModel = shortenModelName(model ?? "", driver);
58
62
  const badge = shortModel ? `${driver}:${shortModel}` : driver;
59
63
  const costStr = cost != null ? ` cost: $${cost.toFixed(3)}` : "";
60
- return `${text}\n[${badge}]${costStr}`;
64
+ return { text: `${text}\n[${badge}]${costStr}`, chatId };
61
65
  }
62
66
  /**
63
67
  * Write a message to the chat log in Redis.
@@ -77,18 +81,32 @@ export function writeChatLog(redis, namespace, msg) {
77
81
  log("warn", "writeChatLog publish failed:", err.message);
78
82
  });
79
83
  }
84
+ /**
85
+ * Resolve the target Discord channelId for a notification.
86
+ * When chatId is set and a reverse-lookup function is available, prefer the originating channel.
87
+ * Falls back to notifyChannelId, then getActiveChannelId.
88
+ */
89
+ function resolveNotifyChannel(chatId, notifyChannelId, getActiveChannelId, reverseSnowflakeLookup) {
90
+ if (chatId != null && reverseSnowflakeLookup) {
91
+ const resolved = reverseSnowflakeLookup(chatId);
92
+ if (resolved)
93
+ return resolved;
94
+ }
95
+ return notifyChannelId ?? getActiveChannelId?.();
96
+ }
80
97
  /**
81
98
  * Start the Discord notifier.
82
99
  *
83
- * @param bot - CcDiscordBot instance (for sending messages)
84
- * @param notifyChannelId - Discord channel ID to forward notifications to. Pass null to use getActiveChannelId.
85
- * @param namespace - cc-agent namespace (used to build Redis channel names)
86
- * @param redis - ioredis client in normal mode (will be duplicated for pub/sub)
87
- * @param handleUserMessage - Optional callback to feed UI messages into the active Claude session
88
- * @param forwardNotification - Optional callback to forward job notifications
89
- * @param getActiveChannelId - Optional callback to resolve channelId dynamically
100
+ * @param bot - CcDiscordBot instance (for sending messages)
101
+ * @param notifyChannelId - Discord channel ID to forward notifications to. Pass null to use getActiveChannelId.
102
+ * @param namespace - cc-agent namespace (used to build Redis channel names)
103
+ * @param redis - ioredis client in normal mode (will be duplicated for pub/sub)
104
+ * @param handleUserMessage - Optional callback to feed UI messages into the active Claude session
105
+ * @param forwardNotification - Optional callback to forward job notifications
106
+ * @param getActiveChannelId - Optional callback to resolve channelId dynamically
107
+ * @param reverseSnowflakeLookup - Optional callback to resolve a chatId integer to a Discord channelId
90
108
  */
91
- export function startNotifier(bot, notifyChannelId, namespace, redis, handleUserMessage, forwardNotification, getActiveChannelId) {
109
+ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUserMessage, forwardNotification, getActiveChannelId, reverseSnowflakeLookup) {
92
110
  // Per-namespace channelId registry
93
111
  const routedChannelIds = new Map();
94
112
  const sub = redis.duplicate({
@@ -210,12 +228,13 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
210
228
  }
211
229
  }
212
230
  for (const raw of items) {
213
- const text = parseNotification(raw);
214
- bot.sendToChannelById(targetId, text).catch((err) => {
231
+ const notification = parseNotification(raw);
232
+ const destChannelId = resolveNotifyChannel(notification.chatId, notifyChannelId, getActiveChannelId, reverseSnowflakeLookup) ?? targetId;
233
+ bot.sendToChannelById(destChannelId, notification.text).catch((err) => {
215
234
  log("warn", "notify list send failed:", err.message);
216
235
  });
217
236
  if (forwardNotification) {
218
- forwardNotification(targetId, text);
237
+ forwardNotification(destChannelId, notification.text);
219
238
  }
220
239
  }
221
240
  if (remaining > 0) {
@@ -231,14 +250,14 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
231
250
  const notifyCh = notifyChannel(namespace);
232
251
  const incomingCh = chatIncomingChannel(namespace);
233
252
  if (channel === notifyCh) {
234
- const targetId = notifyChannelId ?? getActiveChannelId?.();
253
+ const notification = parseNotification(message);
254
+ const targetId = resolveNotifyChannel(notification.chatId, notifyChannelId, getActiveChannelId, reverseSnowflakeLookup);
235
255
  if (targetId != null) {
236
- const text = parseNotification(message);
237
- bot.sendToChannelById(targetId, text).catch((err) => {
256
+ bot.sendToChannelById(targetId, notification.text).catch((err) => {
238
257
  log("warn", "notify send failed:", err.message);
239
258
  });
240
259
  if (forwardNotification) {
241
- forwardNotification(targetId, text);
260
+ forwardNotification(targetId, notification.text);
242
261
  }
243
262
  }
244
263
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-discord",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Claude Code Discord bot — chat with Claude Code via Discord",
5
5
  "type": "module",
6
6
  "bin": {