@hoverlover/cc-discord 0.5.5 → 0.5.7

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Discord <-> Claude Code relay** — power per-channel AI Agents using your existing Claude subscription (no API key needed).
4
4
 
5
- - One autonomous Claude Code agent per Discord channel
5
+ - One autonomous Claude Code agent per Discord channel — plus per-thread agents for threaded conversations
6
6
  - Messages stored in SQLite, delivered to agents via hooks
7
7
  - Replies sent back to Discord via `send-discord` tool
8
8
  - Automatic catch-up of missed messages on startup — no messages lost during restarts
@@ -153,7 +153,8 @@ These are read by the orchestrator shell script:
153
153
 
154
154
  | Variable | Default | Description |
155
155
  |---|---|---|
156
- | `HEALTH_CHECK_INTERVAL` | `30` | Seconds between health checks |
156
+ | `HEALTH_CHECK_INTERVAL` | `30` | Seconds between full health checks |
157
+ | `UNSERVICED_CHECK_INTERVAL` | `5` | Seconds between checks for new threads/channels needing agents |
157
158
  | `AGENT_RESTART_DELAY` | `5` | Seconds to wait before restarting a dead agent |
158
159
  | `CC_DISCORD_CONFIG_DIR` | `~/.config/cc-discord` | Directory for user config env files |
159
160
  | `CC_DISCORD_LOG_DIR` | `/tmp/cc-discord/logs` | Directory for all log files |
@@ -172,10 +173,10 @@ Security note: the worker process intentionally does not receive `DISCORD_BOT_TO
172
173
  │ │ Relay Server │ │ Shell Orchestrator │ │
173
174
  │ │ (bun + express) │ │ (orchestrator.sh) │ │
174
175
  │ │ │ │ │ │
175
- │ │ - Discord bot │ │ Discovers channels via API │ │
176
+ │ │ - Discord bot │ │ Discovers channels/threads │ │
176
177
  │ │ - HTTP API │ │ Spawns 1 Claude agent per │ │
177
- │ │ - SQLite store │ │ channel, monitors health, │ │
178
- │ │ - Typing mgr │ │ restarts dead/stuck agents │ │
178
+ │ │ - SQLite store │ │ channel or thread, monitors│ │
179
+ │ │ - Typing mgr │ │ health, restarts if stuck │ │
179
180
  │ │ - Trace threads │ │ │ │
180
181
  │ └────────┬─────────┘ │ ┌────────┐ ┌────────┐ │ │
181
182
  │ │ │ │Agent #1│ │Agent #2│ ... │ │
@@ -199,9 +200,21 @@ Security note: the worker process intentionally does not receive `DISCORD_BOT_TO
199
200
 
200
201
  ## Features
201
202
 
203
+ ### Thread support
204
+
205
+ Threads in allowed channels are automatically supported — no configuration needed. When a user posts in a thread, the relay detects it, and the orchestrator spawns a dedicated agent for that thread within seconds. Each thread gets its own independent conversation context, separate from the parent channel.
206
+
207
+ - **Automatic discovery:** The orchestrator polls for unserviced messages every 5s (`UNSERVICED_CHECK_INTERVAL`), so new threads get an agent almost immediately
208
+ - **Thread context in messages:** Agents see thread messages tagged with the thread name (e.g. `user [thread: Bug Discussion]: message text`), so they know they're in a thread
209
+ - **Catch-up:** Active threads are included in the startup catch-up alongside channels
210
+ - **Typing & replies:** Typing indicators and replies work inside threads just like in channels
211
+ - **Trace thread exclusion:** Bot-managed trace threads are never treated as user conversation threads
212
+
213
+ Threads in allowed channels are permitted automatically — you don't need to add thread IDs to `DISCORD_ALLOWED_CHANNEL_IDS`.
214
+
202
215
  ### Message catch-up
203
216
 
204
- When the relay starts, it fetches recent message history from each allowed channel and persists any messages that arrived while it was offline. Duplicates are silently ignored. This ensures no messages are lost during restarts or outages. Controlled by `CATCHUP_MESSAGE_LIMIT` (default 100, set to `0` to disable).
217
+ When the relay starts, it fetches recent message history from each allowed channel (and their active threads) and persists any messages that arrived while it was offline. Duplicates are silently ignored. This ensures no messages are lost during restarts or outages. Controlled by `CATCHUP_MESSAGE_LIMIT` (default 100, set to `0` to disable).
205
218
 
206
219
  ### Typing indicators
207
220
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hoverlover/cc-discord",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "Discord <-> Claude Code relay: use your Claude subscription to power per-channel AI bots",
5
5
  "type": "module",
6
6
  "bin": {
package/server/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { Client, GatewayIntentBits, REST, Routes, SlashCommandBuilder } from "discord.js";
3
+ import { Client, GatewayIntentBits, MessageType, REST, Routes, SlashCommandBuilder } from "discord.js";
4
4
  import express, { type NextFunction, type Request, type Response } from "express";
5
5
  import { cleanupOldAttachments } from "./attachment.ts";
6
6
  import { maybeNotifyBusyQueued } from "./busy-notify.ts";
@@ -105,6 +105,7 @@ client.once("clientReady", async () => {
105
105
  client.on("messageCreate", async (message) => {
106
106
  if (!message) return;
107
107
  if (message.author?.bot) return;
108
+ if (message.type === MessageType.ThreadCreated || message.type === MessageType.ThreadStarterMessage) return;
108
109
  if (!isAllowedChannelForMessage(message)) return;
109
110
  if (message.channel?.isThread?.() && isTraceThread(message.channelId)) return;
110
111
  if (!isAllowedUser(message.author?.id)) {
@@ -54,6 +54,18 @@ async function ensureTraceThread(client: Client, channelId: string): Promise<Thr
54
54
  const failedAt = failedChannels.get(channelId);
55
55
  if (failedAt && Date.now() - failedAt < FAILURE_COOLDOWN_MS) return null;
56
56
 
57
+ // Quick-fetch to detect threads: can't create trace threads inside threads
58
+ try {
59
+ const ch = await client.channels.fetch(channelId);
60
+ if (ch?.isThread()) {
61
+ failedChannels.set(channelId, Date.now());
62
+ return null;
63
+ }
64
+ } catch {
65
+ // Channel doesn't exist or inaccessible — fall through to cached/DB path
66
+ // which will also fail and trigger cooldown
67
+ }
68
+
57
69
  // Check in-memory cache first
58
70
  const cachedThreadId = threadCache.get(channelId);
59
71
  if (cachedThreadId) {
@@ -156,7 +168,13 @@ async function ensureTraceThread(client: Client, channelId: string): Promise<Thr
156
168
  threadCache.set(channelId, thread.id);
157
169
  return thread;
158
170
  } catch (err) {
159
- console.error(`[Trace] Failed to create trace thread for channel ${channelId}:`, err);
171
+ const code = (err as any)?.code;
172
+ if (code === 10003 || code === 50001 || code === 50013) {
173
+ console.warn(`[Trace] Cannot create trace thread for channel ${channelId} (${code}) — backing off 5m`);
174
+ failedChannels.set(channelId, Date.now());
175
+ } else {
176
+ console.error(`[Trace] Failed to create trace thread for channel ${channelId}:`, err);
177
+ }
160
178
  return null;
161
179
  }
162
180
  }
@@ -313,9 +331,9 @@ async function flushTraceEvents(client: Client) {
313
331
  postedIds.push(...channelEvents.map((e) => e.id));
314
332
  } catch (err) {
315
333
  const code = (err as any)?.code;
316
- if (code === 50001 || code === 50013) {
317
- // Missing Access or Missing Permissions — clear cache and back off
318
- console.warn(`[Trace] No access to trace thread for channel ${channelId} (${code}) — backing off 5m`);
334
+ if (code === 50001 || code === 50013 || code === 10003) {
335
+ // Missing Access, Missing Permissions, or Unknown Channel — clear cache and back off
336
+ console.warn(`[Trace] Cannot access trace thread for channel ${channelId} (${code}) — backing off 5m`);
319
337
  threadCache.delete(channelId);
320
338
  failedChannels.set(channelId, Date.now());
321
339
  // Mark as posted to avoid infinite retry on permanent access errors