@hoverlover/cc-discord 0.3.0 → 0.3.2
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/.env.worker.example +1 -3
- package/LICENSE +21 -0
- package/README.md +13 -0
- package/hooks/safe-bash.ts +1 -2
- package/package.json +1 -1
- package/scripts/channel-agent.sh +0 -1
- package/server/trace-thread.ts +44 -2
package/.env.worker.example
CHANGED
|
@@ -30,10 +30,8 @@ BASH_POLICY_MODE=block
|
|
|
30
30
|
ALLOW_BASH_RUN_IN_BACKGROUND=true
|
|
31
31
|
ALLOW_BASH_BACKGROUND_OPS=false
|
|
32
32
|
|
|
33
|
-
# Notify Discord when safety policy is triggered
|
|
33
|
+
# Notify Discord when safety policy is triggered (sent to the agent's own channel)
|
|
34
34
|
BASH_POLICY_NOTIFY_ON_BLOCK=true
|
|
35
|
-
# Optional override channel for policy notifications (defaults to relay channel)
|
|
36
|
-
BASH_POLICY_NOTIFY_CHANNEL_ID=
|
|
37
35
|
|
|
38
36
|
# Stuck agent detection: seconds without a heartbeat + unread messages = stuck.
|
|
39
37
|
# Default 900 (15 min). Set higher if agents routinely do long tasks.
|
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/hooks/safe-bash.ts
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
* - ALLOW_BASH_RUN_IN_BACKGROUND=true|false (default: true)
|
|
13
13
|
* - ALLOW_BASH_BACKGROUND_OPS=true|false (default: false)
|
|
14
14
|
* - BASH_POLICY_NOTIFY_ON_BLOCK=true|false (default: true)
|
|
15
|
-
* - BASH_POLICY_NOTIFY_CHANNEL_ID=<discord-channel-id> (optional)
|
|
16
15
|
*
|
|
17
16
|
* When blocked, script exits 2 (Claude Code hook "block" behavior).
|
|
18
17
|
*/
|
|
@@ -74,7 +73,7 @@ async function notifyDiscord({ blocked, reasons, command }: { blocked: boolean;
|
|
|
74
73
|
const relayPort = process.env.RELAY_PORT || "3199";
|
|
75
74
|
const relayUrl = process.env.RELAY_URL || `http://${relayHost}:${relayPort}`;
|
|
76
75
|
const apiToken = process.env.RELAY_API_TOKEN || "";
|
|
77
|
-
const channelId = process.env.
|
|
76
|
+
const channelId = process.env.AGENT_ID || null;
|
|
78
77
|
const fromAgent = process.env.AGENT_ID || process.env.CLAUDE_AGENT_ID || "bash-guard";
|
|
79
78
|
|
|
80
79
|
const status = blocked ? "blocked" : "warning";
|
package/package.json
CHANGED
package/scripts/channel-agent.sh
CHANGED
|
@@ -37,7 +37,6 @@ WORKER_KEYS=(
|
|
|
37
37
|
ALLOW_BASH_RUN_IN_BACKGROUND
|
|
38
38
|
ALLOW_BASH_BACKGROUND_OPS
|
|
39
39
|
BASH_POLICY_NOTIFY_ON_BLOCK
|
|
40
|
-
BASH_POLICY_NOTIFY_CHANNEL_ID
|
|
41
40
|
)
|
|
42
41
|
load_env_keys "$ROOT_DIR/.env.worker" "${WORKER_KEYS[@]}"
|
|
43
42
|
load_env_keys "${CC_DISCORD_CONFIG_DIR:-$HOME/.config/cc-discord}/.env.worker" "${WORKER_KEYS[@]}"
|
package/server/trace-thread.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
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);
|