@gonzih/cc-discord 0.1.0 → 0.1.1
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 +7 -0
- package/dist/bot.js +95 -2
- package/dist/router.d.ts +14 -1
- package/dist/router.js +18 -1
- package/package.json +1 -1
package/dist/bot.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export declare class CcDiscordBot {
|
|
|
33
33
|
constructor(opts: DiscordBotOptions);
|
|
34
34
|
/** Reverse-lookup: find the channelId string for a cron-stored integer */
|
|
35
35
|
private snowflakeMap;
|
|
36
|
+
/** Channels created by the bot for a meta-agent namespace → skip local Claude session */
|
|
37
|
+
private channelNamespaceMap;
|
|
36
38
|
private storeSnowflake;
|
|
37
39
|
private reverseSnowflakeLookup;
|
|
38
40
|
/** Session key: "channelId" or "channelId:threadId" for threads */
|
|
@@ -64,6 +66,11 @@ export declare class CcDiscordBot {
|
|
|
64
66
|
*/
|
|
65
67
|
callCcAgentTool(toolName: string, args?: Record<string, unknown>): Promise<string | null>;
|
|
66
68
|
private runCronTask;
|
|
69
|
+
/**
|
|
70
|
+
* Create a new Discord text channel for `namespace`, register it in channelNamespaceMap,
|
|
71
|
+
* and start the meta-agent for `repoUrl`. Fire-and-forget after sending the confirmation message.
|
|
72
|
+
*/
|
|
73
|
+
private createChannelForRepo;
|
|
67
74
|
/** Write a message to the Redis chat log. Fire-and-forget. */
|
|
68
75
|
private writeChatMessage;
|
|
69
76
|
/** Returns the last channelId that sent a message. */
|
package/dist/bot.js
CHANGED
|
@@ -13,7 +13,7 @@ import { formatForDiscord, splitLongMessage, stripAnsi } from "./formatter.js";
|
|
|
13
13
|
import { getCurrentToken } from "./tokens.js";
|
|
14
14
|
import { writeChatLog } from "./notifier.js";
|
|
15
15
|
import { CronManager } from "./cron.js";
|
|
16
|
-
import { parseRoutingTag, ensureMetaAgent, routeToMetaAgent } from "./router.js";
|
|
16
|
+
import { parseRoutingTag, parseChannelCreateIntent, ensureMetaAgent, routeToMetaAgent } from "./router.js";
|
|
17
17
|
import { metaAgentStatusKey } from "@gonzih/cc-wire";
|
|
18
18
|
/** Convert a Discord snowflake string to a safe 53-bit integer for CronManager compatibility. */
|
|
19
19
|
function snowflakeToInt(id) {
|
|
@@ -139,6 +139,8 @@ export class CcDiscordBot {
|
|
|
139
139
|
}
|
|
140
140
|
/** Reverse-lookup: find the channelId string for a cron-stored integer */
|
|
141
141
|
snowflakeMap = new Map();
|
|
142
|
+
/** Channels created by the bot for a meta-agent namespace → skip local Claude session */
|
|
143
|
+
channelNamespaceMap = new Map();
|
|
142
144
|
storeSnowflake(channelId) {
|
|
143
145
|
const n = snowflakeToInt(channelId);
|
|
144
146
|
this.snowflakeMap.set(n, channelId);
|
|
@@ -255,6 +257,27 @@ export class CcDiscordBot {
|
|
|
255
257
|
text = text.replace(/<@!?\d+>/g, "").trim();
|
|
256
258
|
if (!text)
|
|
257
259
|
return;
|
|
260
|
+
// Natural-language channel creation: "channel for https://github.com/org/repo"
|
|
261
|
+
if (this.redis) {
|
|
262
|
+
const intent = parseChannelCreateIntent(text);
|
|
263
|
+
if (intent) {
|
|
264
|
+
await this.createChannelForRepo(msg, intent.namespace, intent.repoUrl);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Channel registered via createChannelForRepo or /channel — route directly to its meta-agent
|
|
269
|
+
const mappedNs = this.channelNamespaceMap.get(effectiveChannelId);
|
|
270
|
+
if (mappedNs && this.redis) {
|
|
271
|
+
this.writeChatMessage("user", "discord", text, effectiveChannelId);
|
|
272
|
+
this.opts.registerRoutedChannelId?.(mappedNs.namespace, effectiveChannelId);
|
|
273
|
+
try {
|
|
274
|
+
await routeToMetaAgent(mappedNs.namespace, text, this.redis);
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
await msg.channel.send(`Failed to route to ${mappedNs.namespace}: ${err.message}`).catch(() => { });
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
258
281
|
// #tag / #org/repo routing — delegate to meta-agent
|
|
259
282
|
if (this.redis) {
|
|
260
283
|
const routing = parseRoutingTag(text);
|
|
@@ -463,7 +486,9 @@ export class CcDiscordBot {
|
|
|
463
486
|
session.flushTimer = null;
|
|
464
487
|
if (!text)
|
|
465
488
|
return;
|
|
466
|
-
|
|
489
|
+
// Use source="discord" so the notifier's pmessage guard (source !== "claude") drops it
|
|
490
|
+
// and does not re-send this message as a second Discord notification.
|
|
491
|
+
this.writeChatMessage("assistant", "discord", text, channelId);
|
|
467
492
|
await this.sendToChannel(channel, text);
|
|
468
493
|
}
|
|
469
494
|
startTyping(channelId, channel, session) {
|
|
@@ -535,6 +560,10 @@ export class CcDiscordBot {
|
|
|
535
560
|
.setName("wiki")
|
|
536
561
|
.setDescription("Wiki page info (pass namespace to look up)")
|
|
537
562
|
.addStringOption((opt) => opt.setName("namespace").setDescription("Namespace to look up").setRequired(false)),
|
|
563
|
+
new SlashCommandBuilder()
|
|
564
|
+
.setName("channel")
|
|
565
|
+
.setDescription("Create a Discord channel for a GitHub repo meta-agent")
|
|
566
|
+
.addStringOption((opt) => opt.setName("repo").setDescription("GitHub repo URL (e.g. https://github.com/org/repo)").setRequired(true)),
|
|
538
567
|
].map((cmd) => cmd.toJSON());
|
|
539
568
|
const rest = new REST().setToken(this.opts.discordToken);
|
|
540
569
|
if (this.opts.guildIds?.length) {
|
|
@@ -624,6 +653,39 @@ export class CcDiscordBot {
|
|
|
624
653
|
}
|
|
625
654
|
break;
|
|
626
655
|
}
|
|
656
|
+
case "channel": {
|
|
657
|
+
const repoUrl = interaction.options.getString("repo", true);
|
|
658
|
+
const urlMatch = repoUrl.match(/^https?:\/\/github\.com\/([\w.-]+)\/([\w.-]+)/i);
|
|
659
|
+
if (!urlMatch) {
|
|
660
|
+
await interaction.reply({ content: "Invalid repo URL. Use: https://github.com/org/repo", ephemeral: true });
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const namespace = urlMatch[2];
|
|
664
|
+
const guild = interaction.guild;
|
|
665
|
+
if (!guild) {
|
|
666
|
+
await interaction.reply({ content: "Channel creation requires a guild (not available in DMs).", ephemeral: true });
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
await interaction.deferReply();
|
|
670
|
+
try {
|
|
671
|
+
const newChannel = await guild.channels.create({ name: namespace, type: ChannelType.GuildText });
|
|
672
|
+
this.channelNamespaceMap.set(newChannel.id, { namespace, repoUrl });
|
|
673
|
+
this.opts.registerRoutedChannelId?.(namespace, newChannel.id);
|
|
674
|
+
await interaction.editReply(`Created <#${newChannel.id}> — messages there route to the ${repoUrl} meta-agent`);
|
|
675
|
+
// Start meta-agent in the background
|
|
676
|
+
if (this.redis) {
|
|
677
|
+
ensureMetaAgent(namespace, repoUrl, (toolName, args) => this.callCcAgentTool(toolName, args ?? {}), this.redis)
|
|
678
|
+
.catch((err) => {
|
|
679
|
+
console.error(`[bot] /channel ensureMetaAgent(${namespace}) failed:`, err.message);
|
|
680
|
+
this.sendToChannelById(newChannel.id, `Warning: meta-agent startup failed — ${err.message}`).catch(() => { });
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
catch (err) {
|
|
685
|
+
await interaction.editReply(`Failed to create channel: ${err.message}`);
|
|
686
|
+
}
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
627
689
|
}
|
|
628
690
|
}
|
|
629
691
|
async handleCronsCommand(interaction, channelId) {
|
|
@@ -727,6 +789,37 @@ export class CcDiscordBot {
|
|
|
727
789
|
}
|
|
728
790
|
})();
|
|
729
791
|
}
|
|
792
|
+
/**
|
|
793
|
+
* Create a new Discord text channel for `namespace`, register it in channelNamespaceMap,
|
|
794
|
+
* and start the meta-agent for `repoUrl`. Fire-and-forget after sending the confirmation message.
|
|
795
|
+
*/
|
|
796
|
+
async createChannelForRepo(msg, namespace, repoUrl) {
|
|
797
|
+
const channel = msg.channel;
|
|
798
|
+
const guild = msg.guild;
|
|
799
|
+
if (!guild) {
|
|
800
|
+
await channel.send("Channel creation requires a guild (not available in DMs).").catch(() => { });
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
let newChannel;
|
|
804
|
+
try {
|
|
805
|
+
newChannel = await guild.channels.create({ name: namespace, type: ChannelType.GuildText });
|
|
806
|
+
}
|
|
807
|
+
catch (err) {
|
|
808
|
+
await channel.send(`Failed to create channel: ${err.message}`).catch(() => { });
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
this.channelNamespaceMap.set(newChannel.id, { namespace, repoUrl });
|
|
812
|
+
this.opts.registerRoutedChannelId?.(namespace, newChannel.id);
|
|
813
|
+
await channel.send(`Created <#${newChannel.id}> — messages there route to the ${repoUrl} meta-agent`).catch(() => { });
|
|
814
|
+
// Start meta-agent in the background after acknowledging the user
|
|
815
|
+
if (this.redis) {
|
|
816
|
+
ensureMetaAgent(namespace, repoUrl, (toolName, args) => this.callCcAgentTool(toolName, args ?? {}), this.redis)
|
|
817
|
+
.catch((err) => {
|
|
818
|
+
console.error(`[bot] ensureMetaAgent(${namespace}) failed:`, err.message);
|
|
819
|
+
this.sendToChannelById(newChannel.id, `Warning: meta-agent startup failed — ${err.message}`).catch(() => { });
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
}
|
|
730
823
|
/** Write a message to the Redis chat log. Fire-and-forget. */
|
|
731
824
|
writeChatMessage(role, source, content, channelId) {
|
|
732
825
|
if (!this.redis)
|
package/dist/router.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Routing helpers: hashtag meta-agent routing and channel-creation intent detection.
|
|
3
3
|
*
|
|
4
4
|
* Parses #tag or #org/repo tokens from Telegram messages and routes them to
|
|
5
5
|
* the appropriate cc-agent meta-agent instead of the local Claude session.
|
|
@@ -50,6 +50,19 @@ export declare function parseRoutingTag(text: string): RoutingTag | null;
|
|
|
50
50
|
* Throws on failure (repo creation error, tool call failure, or timeout).
|
|
51
51
|
*/
|
|
52
52
|
export declare function ensureMetaAgent(namespace: string, repoUrl: string, callTool: CallToolFn, redis: Redis): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Detect a natural-language channel-creation request.
|
|
55
|
+
* Matches:
|
|
56
|
+
* "channel for https://github.com/org/repo"
|
|
57
|
+
* "create channel for https://github.com/org/repo"
|
|
58
|
+
* "add channel for https://github.com/org/repo"
|
|
59
|
+
*
|
|
60
|
+
* Returns { namespace, repoUrl } or null.
|
|
61
|
+
*/
|
|
62
|
+
export declare function parseChannelCreateIntent(text: string): {
|
|
63
|
+
namespace: string;
|
|
64
|
+
repoUrl: string;
|
|
65
|
+
} | null;
|
|
53
66
|
/**
|
|
54
67
|
* Route a message to a running meta-agent via Redis RPUSH.
|
|
55
68
|
* The cc-agent polls cca:meta:{namespace}:input every 3s (up to 3s delivery latency).
|
package/dist/router.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Routing helpers: hashtag meta-agent routing and channel-creation intent detection.
|
|
3
3
|
*
|
|
4
4
|
* Parses #tag or #org/repo tokens from Telegram messages and routes them to
|
|
5
5
|
* the appropriate cc-agent meta-agent instead of the local Claude session.
|
|
@@ -173,6 +173,23 @@ export async function ensureMetaAgent(namespace, repoUrl, callTool, redis) {
|
|
|
173
173
|
}
|
|
174
174
|
throw new Error(`Meta-agent for ${namespace} did not become ready within ${timeoutMs}ms`);
|
|
175
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Detect a natural-language channel-creation request.
|
|
178
|
+
* Matches:
|
|
179
|
+
* "channel for https://github.com/org/repo"
|
|
180
|
+
* "create channel for https://github.com/org/repo"
|
|
181
|
+
* "add channel for https://github.com/org/repo"
|
|
182
|
+
*
|
|
183
|
+
* Returns { namespace, repoUrl } or null.
|
|
184
|
+
*/
|
|
185
|
+
export function parseChannelCreateIntent(text) {
|
|
186
|
+
const match = text.match(/(?:create\s+|add\s+)?channel\s+for\s+(https?:\/\/github\.com\/([\w.-]+)\/([\w.-]+))/i);
|
|
187
|
+
if (!match)
|
|
188
|
+
return null;
|
|
189
|
+
const repoUrl = match[1];
|
|
190
|
+
const namespace = match[3];
|
|
191
|
+
return { namespace, repoUrl };
|
|
192
|
+
}
|
|
176
193
|
/**
|
|
177
194
|
* Route a message to a running meta-agent via Redis RPUSH.
|
|
178
195
|
* The cc-agent polls cca:meta:{namespace}:input every 3s (up to 3s delivery latency).
|