@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 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
- this.writeChatMessage("assistant", "claude", text, channelId);
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
- * Hashtag meta-agent routing.
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
- * Hashtag meta-agent routing.
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).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-discord",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Claude Code Discord bot — chat with Claude Code via Discord",
5
5
  "type": "module",
6
6
  "bin": {