@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 +1 -1
- package/dist/bot.js +23 -3
- package/dist/index.js +1 -1
- package/dist/notifier.d.ts +17 -11
- package/dist/notifier.js +39 -20
- package/package.json +1 -1
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
|
-
|
|
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) =>
|
|
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();
|
package/dist/notifier.d.ts
CHANGED
|
@@ -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
|
|
25
|
-
*
|
|
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):
|
|
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
|
|
46
|
-
* @param notifyChannelId
|
|
47
|
-
* @param namespace
|
|
48
|
-
* @param redis
|
|
49
|
-
* @param handleUserMessage
|
|
50
|
-
* @param forwardNotification
|
|
51
|
-
* @param getActiveChannelId
|
|
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
|
|
35
|
-
*
|
|
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
|
|
84
|
-
* @param notifyChannelId
|
|
85
|
-
* @param namespace
|
|
86
|
-
* @param redis
|
|
87
|
-
* @param handleUserMessage
|
|
88
|
-
* @param forwardNotification
|
|
89
|
-
* @param getActiveChannelId
|
|
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
|
|
214
|
-
|
|
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(
|
|
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
|
|
253
|
+
const notification = parseNotification(message);
|
|
254
|
+
const targetId = resolveNotifyChannel(notification.chatId, notifyChannelId, getActiveChannelId, reverseSnowflakeLookup);
|
|
235
255
|
if (targetId != null) {
|
|
236
|
-
|
|
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 {
|