@hoverlover/cc-discord 0.3.0 → 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Chad Boyd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  - One autonomous Claude Code agent per Discord channel
6
6
  - Messages stored in SQLite, delivered to agents via hooks
7
7
  - Replies sent back to Discord via `send-discord` tool
8
+ - Automatic catch-up of missed messages on startup — no messages lost during restarts
8
9
  - Typing indicators, busy notifications, live trace threads, memory context, attachment support, and more
9
10
 
10
11
  ## Quick start
@@ -198,6 +199,10 @@ Security note: the worker process intentionally does not receive `DISCORD_BOT_TO
198
199
 
199
200
  ## Features
200
201
 
202
+ ### Message catch-up
203
+
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).
205
+
201
206
  ### Typing indicators
202
207
 
203
208
  When a user sends a message, the relay starts a typing indicator that repeats every `TYPING_INTERVAL_MS` (default 8s). It stops automatically when Claude replies. After `TYPING_MAX_MS` (default 120s), a configurable fallback patience message is sent.
@@ -266,6 +271,8 @@ In interactive mode, the orchestrator runs as a Claude Code session with a visib
266
271
 
267
272
  To keep cc-discord running across reboots and terminal closures, install it as a system service.
268
273
 
274
+ > **Dedicated Mac server?** If you're setting up a Mac mini (or similar) as a headless, always-on server, see the [macOS Headless Server Setup Guide](docs/mac-setup-guide.md). It covers disabling SIP, configuring TCC permissions, FileVault tradeoffs, auto-login, and LaunchDaemon setup for fully unattended operation.
275
+
269
276
  ### macOS (launchd)
270
277
 
271
278
  1. Find the full paths to `bunx` and `claude`:
@@ -463,3 +470,9 @@ Monitor all logs:
463
470
  ```bash
464
471
  tail -f /tmp/cc-discord/logs/*.log
465
472
  ```
473
+
474
+ ---
475
+
476
+ ## License
477
+
478
+ [MIT](LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hoverlover/cc-discord",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Discord <-> Claude Code relay: use your Claude subscription to power per-channel AI bots",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,6 +36,15 @@ let flushTimer: ReturnType<typeof setInterval> | null = null;
36
36
 
37
37
  // ── Thread lifecycle ────────────────────────────────────────────────
38
38
 
39
+ /** Unarchive a thread if it's been auto-archived by Discord. */
40
+ async function ensureUnarchived(thread: ThreadChannel): Promise<void> {
41
+ try {
42
+ if (thread.archived) await thread.setArchived(false);
43
+ } catch {
44
+ // best-effort — bot may lack MANAGE_THREADS
45
+ }
46
+ }
47
+
39
48
  /** Lock a thread if not already locked (idempotent). */
40
49
  async function ensureLocked(thread: ThreadChannel): Promise<void> {
41
50
  try {
@@ -60,7 +69,8 @@ async function ensureTraceThread(client: Client, channelId: string): Promise<Thr
60
69
  try {
61
70
  const thread = await client.channels.fetch(cachedThreadId);
62
71
  if (thread && thread.isThread()) {
63
- // Ensure thread is locked (idempotent handles pre-existing unlocked threads)
72
+ // Unarchive if Discord auto-archived it, then ensure locked
73
+ await ensureUnarchived(thread as ThreadChannel);
64
74
  await ensureLocked(thread as ThreadChannel);
65
75
  return thread as ThreadChannel;
66
76
  }
@@ -76,7 +86,8 @@ async function ensureTraceThread(client: Client, channelId: string): Promise<Thr
76
86
  try {
77
87
  const thread = await client.channels.fetch(dbThreadId);
78
88
  if (thread && thread.isThread()) {
79
- // Ensure thread is locked
89
+ // Unarchive if Discord auto-archived it, then ensure locked
90
+ await ensureUnarchived(thread as ThreadChannel);
80
91
  await ensureLocked(thread as ThreadChannel);
81
92
  threadCache.set(channelId, dbThreadId);
82
93
  return thread as ThreadChannel;
@@ -86,6 +97,37 @@ async function ensureTraceThread(client: Client, channelId: string): Promise<Thr
86
97
  }
87
98
  }
88
99
 
100
+ // Search existing threads by name before creating a new one (fallback for
101
+ // cases where the DB record was lost but the thread still exists in Discord)
102
+ try {
103
+ const channel = await client.channels.fetch(channelId);
104
+ if (channel && channel.type === ChannelType.GuildText) {
105
+ const textChannel = channel as TextChannel;
106
+ // Check active threads
107
+ const activeThreads = await textChannel.threads.fetchActive();
108
+ const existing = activeThreads.threads.find((t) => t.name === TRACE_THREAD_NAME);
109
+ if (existing) {
110
+ await ensureUnarchived(existing);
111
+ await ensureLocked(existing);
112
+ setTraceThreadId(channelId, existing.id);
113
+ threadCache.set(channelId, existing.id);
114
+ return existing;
115
+ }
116
+ // Check archived threads
117
+ const archivedThreads = await textChannel.threads.fetchArchived({ limit: 10 });
118
+ const existingArchived = archivedThreads.threads.find((t) => t.name === TRACE_THREAD_NAME);
119
+ if (existingArchived) {
120
+ await ensureUnarchived(existingArchived);
121
+ await ensureLocked(existingArchived);
122
+ setTraceThreadId(channelId, existingArchived.id);
123
+ threadCache.set(channelId, existingArchived.id);
124
+ return existingArchived;
125
+ }
126
+ }
127
+ } catch {
128
+ // best effort — fall through to create
129
+ }
130
+
89
131
  // Create new thread
90
132
  try {
91
133
  const channel = await client.channels.fetch(channelId);