@hoverlover/cc-discord 0.2.4 → 0.2.5
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
|
@@ -37,7 +37,6 @@ Edit those files with your credentials, then run again. Override the config loca
|
|
|
37
37
|
git clone https://github.com/hoverlover/cc-discord.git
|
|
38
38
|
cd cc-discord
|
|
39
39
|
bun install
|
|
40
|
-
bun run generate-settings
|
|
41
40
|
bun start
|
|
42
41
|
```
|
|
43
42
|
|
|
@@ -262,6 +261,159 @@ In interactive mode, the orchestrator runs as a Claude Code session with a visib
|
|
|
262
261
|
|
|
263
262
|
---
|
|
264
263
|
|
|
264
|
+
## Running as a daemon
|
|
265
|
+
|
|
266
|
+
To keep cc-discord running across reboots and terminal closures, install it as a system service.
|
|
267
|
+
|
|
268
|
+
### macOS (launchd)
|
|
269
|
+
|
|
270
|
+
1. Find the full path to `bunx`:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
which bunx
|
|
274
|
+
# e.g. /Users/you/.bun/bin/bunx
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
2. Create the plist file:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
cat > ~/Library/LaunchAgents/com.cc-discord.plist << 'EOF'
|
|
281
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
282
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
283
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
284
|
+
<plist version="1.0">
|
|
285
|
+
<dict>
|
|
286
|
+
<key>Label</key>
|
|
287
|
+
<string>com.cc-discord</string>
|
|
288
|
+
|
|
289
|
+
<key>ProgramArguments</key>
|
|
290
|
+
<array>
|
|
291
|
+
<!-- Replace with the output of: which bunx -->
|
|
292
|
+
<string>/Users/you/.bun/bin/bunx</string>
|
|
293
|
+
<string>@hoverlover/cc-discord</string>
|
|
294
|
+
</array>
|
|
295
|
+
|
|
296
|
+
<key>EnvironmentVariables</key>
|
|
297
|
+
<dict>
|
|
298
|
+
<key>PATH</key>
|
|
299
|
+
<string>/Users/you/.bun/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
300
|
+
</dict>
|
|
301
|
+
|
|
302
|
+
<key>RunAtLoad</key>
|
|
303
|
+
<true/>
|
|
304
|
+
|
|
305
|
+
<key>KeepAlive</key>
|
|
306
|
+
<true/>
|
|
307
|
+
|
|
308
|
+
<key>StandardOutPath</key>
|
|
309
|
+
<string>/tmp/cc-discord/launchd-stdout.log</string>
|
|
310
|
+
|
|
311
|
+
<key>StandardErrorPath</key>
|
|
312
|
+
<string>/tmp/cc-discord/launchd-stderr.log</string>
|
|
313
|
+
</dict>
|
|
314
|
+
</plist>
|
|
315
|
+
EOF
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
3. Replace `/Users/you/.bun/bin/bunx` and the `PATH` value with your actual paths.
|
|
319
|
+
|
|
320
|
+
4. Load the service:
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
mkdir -p /tmp/cc-discord
|
|
324
|
+
launchctl load ~/Library/LaunchAgents/com.cc-discord.plist
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
5. Manage the service:
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
# Check status
|
|
331
|
+
launchctl list | grep cc-discord
|
|
332
|
+
|
|
333
|
+
# Stop
|
|
334
|
+
launchctl stop com.cc-discord
|
|
335
|
+
|
|
336
|
+
# Start
|
|
337
|
+
launchctl start com.cc-discord
|
|
338
|
+
|
|
339
|
+
# Uninstall
|
|
340
|
+
launchctl unload ~/Library/LaunchAgents/com.cc-discord.plist
|
|
341
|
+
rm ~/Library/LaunchAgents/com.cc-discord.plist
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Linux (systemd)
|
|
345
|
+
|
|
346
|
+
1. Find the full paths to `bunx` and `claude`:
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
which bunx # e.g. /home/you/.bun/bin/bunx
|
|
350
|
+
which claude # e.g. /home/you/.local/bin/claude
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
2. Create the service file:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
sudo cat > /etc/systemd/system/cc-discord.service << 'EOF'
|
|
357
|
+
[Unit]
|
|
358
|
+
Description=cc-discord — Discord <-> Claude Code relay
|
|
359
|
+
After=network-online.target
|
|
360
|
+
Wants=network-online.target
|
|
361
|
+
|
|
362
|
+
[Service]
|
|
363
|
+
Type=simple
|
|
364
|
+
# Replace "you" with your username
|
|
365
|
+
User=you
|
|
366
|
+
# Replace with the output of: which bunx
|
|
367
|
+
ExecStart=/home/you/.bun/bin/bunx @hoverlover/cc-discord
|
|
368
|
+
# Ensure bun and claude are on PATH
|
|
369
|
+
Environment=PATH=/home/you/.bun/bin:/home/you/.local/bin:/usr/local/bin:/usr/bin:/bin
|
|
370
|
+
Environment=HOME=/home/you
|
|
371
|
+
Restart=on-failure
|
|
372
|
+
RestartSec=10
|
|
373
|
+
|
|
374
|
+
[Install]
|
|
375
|
+
WantedBy=multi-user.target
|
|
376
|
+
EOF
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
3. Replace `you` with your actual username and update the paths to match your system.
|
|
380
|
+
|
|
381
|
+
4. Enable and start the service:
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
sudo systemctl daemon-reload
|
|
385
|
+
sudo systemctl enable cc-discord
|
|
386
|
+
sudo systemctl start cc-discord
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
5. Manage the service:
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
# Check status
|
|
393
|
+
systemctl status cc-discord
|
|
394
|
+
|
|
395
|
+
# View logs
|
|
396
|
+
journalctl -u cc-discord -f
|
|
397
|
+
|
|
398
|
+
# Stop
|
|
399
|
+
sudo systemctl stop cc-discord
|
|
400
|
+
|
|
401
|
+
# Restart
|
|
402
|
+
sudo systemctl restart cc-discord
|
|
403
|
+
|
|
404
|
+
# Disable (won't start on boot)
|
|
405
|
+
sudo systemctl disable cc-discord
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Notes
|
|
409
|
+
|
|
410
|
+
- Both approaches run `bunx @hoverlover/cc-discord`, which is the same as `bun start` in the cloned repo.
|
|
411
|
+
- Environment files are read from `~/.config/cc-discord/` — make sure those are configured before starting the service.
|
|
412
|
+
- Application logs still go to `CC_DISCORD_LOG_DIR` (default `/tmp/cc-discord/logs`). The launchd/systemd logs are separate and capture startup errors.
|
|
413
|
+
- Claude CLI must be authenticated (`claude auth login`) as the user the service runs under before starting.
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
265
417
|
## Development
|
|
266
418
|
|
|
267
419
|
### Scripts
|
|
@@ -273,10 +425,8 @@ In interactive mode, the orchestrator runs as a Claude Code session with a visib
|
|
|
273
425
|
| `bun run start:orchestrator` | Start headless orchestrator only |
|
|
274
426
|
| `bun run start:orchestrator-interactive` | Start interactive orchestrator (terminal UI) |
|
|
275
427
|
| `bun run dev` | Alias for `start:relay` |
|
|
276
|
-
| `bun run generate-settings` | Generate `.claude/settings.json` with absolute hook paths |
|
|
277
428
|
| `bun run memory:smoke` | Run memory system smoke test |
|
|
278
429
|
| `bun run memory:inspect` | Inspect memory database contents |
|
|
279
|
-
| `bun run memory:migrate` | Migrate memory to channel-scoped keys |
|
|
280
430
|
| `bun run lint` | Run Biome linter |
|
|
281
431
|
| `bun run lint:fix` | Run Biome linter with auto-fix |
|
|
282
432
|
| `bun run format` | Format code with Biome |
|
|
@@ -284,7 +434,7 @@ In interactive mode, the orchestrator runs as a Claude Code session with a visib
|
|
|
284
434
|
|
|
285
435
|
### Hook system
|
|
286
436
|
|
|
287
|
-
Claude Code hooks are configured in `.claude/settings.json
|
|
437
|
+
Claude Code hooks are configured in `.claude/settings.local.json`, generated automatically from `.claude/settings.template.json` when the relay starts. This file is gitignored and only exists while the relay is running — `start.sh` creates it on startup and removes it on shutdown so hooks don't interfere with normal development. The template uses `__ORCHESTRATOR_DIR__` placeholders that are replaced with absolute paths at generation time.
|
|
288
438
|
|
|
289
439
|
| Hook | Event | Description |
|
|
290
440
|
|---|---|---|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hoverlover/cc-discord",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Discord <-> Claude Code relay: use your Claude subscription to power per-channel AI bots",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,10 +25,8 @@
|
|
|
25
25
|
"start:orchestrator": "bash scripts/orchestrator.sh",
|
|
26
26
|
"start:orchestrator-interactive": "bash scripts/start-orchestrator.sh",
|
|
27
27
|
"dev": "bash scripts/start-relay.sh",
|
|
28
|
-
"generate-settings": "bash scripts/generate-settings.sh",
|
|
29
28
|
"memory:smoke": "bun tools/memory-smoke.ts",
|
|
30
29
|
"memory:inspect": "bun tools/memory-inspect.ts",
|
|
31
|
-
"memory:migrate": "bun scripts/migrate-memory-to-channel-keys.ts",
|
|
32
30
|
"lint": "bunx biome check .",
|
|
33
31
|
"lint:fix": "bunx biome check --write .",
|
|
34
32
|
"format": "bunx biome format --write .",
|
package/scripts/channel-agent.sh
CHANGED
|
@@ -47,7 +47,7 @@ load_env_keys "${CC_DISCORD_CONFIG_DIR:-$HOME/.config/cc-discord}/.env" "${WORKE
|
|
|
47
47
|
# Ensure bun is on PATH for hooks/tools
|
|
48
48
|
export PATH="$HOME/.bun/bin:/opt/homebrew/bin:/usr/local/bin:$ROOT_DIR/tools:$PATH"
|
|
49
49
|
|
|
50
|
-
SETTINGS_PATH="$ROOT_DIR/.claude/settings.json"
|
|
50
|
+
SETTINGS_PATH="$ROOT_DIR/.claude/settings.local.json"
|
|
51
51
|
PROMPT_TEMPLATE="$ROOT_DIR/prompts/channel-system.md"
|
|
52
52
|
|
|
53
53
|
if ! command -v claude >/dev/null 2>&1; then
|
|
@@ -11,7 +11,7 @@ SCRIPT_DIR="$(cd "$(dirname "$_SCRIPT")" && pwd)"
|
|
|
11
11
|
ORCHESTRATOR_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
12
12
|
|
|
13
13
|
TEMPLATE="$ORCHESTRATOR_DIR/.claude/settings.template.json"
|
|
14
|
-
OUTPUT="$ORCHESTRATOR_DIR/.claude/settings.json"
|
|
14
|
+
OUTPUT="$ORCHESTRATOR_DIR/.claude/settings.local.json"
|
|
15
15
|
|
|
16
16
|
if [ ! -f "$TEMPLATE" ]; then
|
|
17
17
|
echo "Template not found: $TEMPLATE"
|
|
@@ -34,7 +34,7 @@ load_env_keys "${CC_DISCORD_CONFIG_DIR:-$HOME/.config/cc-discord}/.env.worker" "
|
|
|
34
34
|
load_env_keys "$ROOT_DIR/.env" "${WORKER_KEYS[@]}"
|
|
35
35
|
load_env_keys "${CC_DISCORD_CONFIG_DIR:-$HOME/.config/cc-discord}/.env" "${WORKER_KEYS[@]}"
|
|
36
36
|
|
|
37
|
-
SETTINGS_PATH="$ROOT_DIR/.claude/settings.json"
|
|
37
|
+
SETTINGS_PATH="$ROOT_DIR/.claude/settings.local.json"
|
|
38
38
|
SYSTEM_PROMPT_PATH="$ROOT_DIR/prompts/orchestrator-system.md"
|
|
39
39
|
|
|
40
40
|
if ! command -v claude >/dev/null 2>&1; then
|
package/scripts/start.sh
CHANGED
|
@@ -85,11 +85,11 @@ if [ -d "$ROOT_DIR/.claude/skills" ] && [ "$ROOT_DIR" != "$CC_DISCORD_HOME" ]; t
|
|
|
85
85
|
done
|
|
86
86
|
fi
|
|
87
87
|
|
|
88
|
-
#
|
|
89
|
-
if [
|
|
88
|
+
# Generate settings.local.json (hooks + relay permissions) so Claude agents see project config.
|
|
89
|
+
if [ -f "$ROOT_DIR/.claude/settings.template.json" ]; then
|
|
90
90
|
bash "$ROOT_DIR/scripts/generate-settings.sh"
|
|
91
|
-
if [ -f "$ROOT_DIR/.claude/settings.json" ]; then
|
|
92
|
-
cp "$ROOT_DIR/.claude/settings.json" "$CC_DISCORD_HOME/.claude/settings.json"
|
|
91
|
+
if [ "$ROOT_DIR" != "$CC_DISCORD_HOME" ] && [ -f "$ROOT_DIR/.claude/settings.local.json" ]; then
|
|
92
|
+
cp "$ROOT_DIR/.claude/settings.local.json" "$CC_DISCORD_HOME/.claude/settings.local.json"
|
|
93
93
|
fi
|
|
94
94
|
fi
|
|
95
95
|
|
|
@@ -120,6 +120,10 @@ cleanup() {
|
|
|
120
120
|
wait "$RELAY_PID" 2>/dev/null || true
|
|
121
121
|
fi
|
|
122
122
|
|
|
123
|
+
# Remove generated settings.local.json so hooks don't fire during normal development
|
|
124
|
+
rm -f "$ROOT_DIR/.claude/settings.local.json"
|
|
125
|
+
rm -f "$CC_DISCORD_HOME/.claude/settings.local.json"
|
|
126
|
+
|
|
123
127
|
log "All processes stopped."
|
|
124
128
|
exit 0
|
|
125
129
|
}
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* One-time migration: re-key memory_turns from the legacy shared session key
|
|
4
|
-
* (discord:default:claude-discord) into per-channel session keys
|
|
5
|
-
* (discord:default:{channelId}).
|
|
6
|
-
*
|
|
7
|
-
* Each turn's channelId is read from its metadata_json. Turns without a
|
|
8
|
-
* channelId are left in the old key.
|
|
9
|
-
*
|
|
10
|
-
* Turn indices are renumbered per-channel to be sequential, starting after
|
|
11
|
-
* any existing turns already in that channel key.
|
|
12
|
-
*
|
|
13
|
-
* Usage:
|
|
14
|
-
* bun scripts/migrate-memory-to-channel-keys.ts [path/to/memory.db]
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { Database } from "bun:sqlite";
|
|
18
|
-
import { dirname, join } from "node:path";
|
|
19
|
-
import { fileURLToPath } from "node:url";
|
|
20
|
-
|
|
21
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
|
-
const defaultDataDir = process.env.CC_DISCORD_DATA_DIR || join(process.env.HOME || "", ".cc-discord", "data");
|
|
23
|
-
const dbPath = process.argv[2] || join(defaultDataDir, "memory.db");
|
|
24
|
-
|
|
25
|
-
console.log(`[migrate] Opening ${dbPath}`);
|
|
26
|
-
const db = new Database(dbPath);
|
|
27
|
-
|
|
28
|
-
const OLD_SESSION_KEY = "discord:default:claude-discord";
|
|
29
|
-
|
|
30
|
-
// Read all turns under the old key
|
|
31
|
-
const turns = db
|
|
32
|
-
.prepare(`
|
|
33
|
-
SELECT id, session_key, turn_index, role, content, metadata_json, created_at
|
|
34
|
-
FROM memory_turns
|
|
35
|
-
WHERE session_key = ?
|
|
36
|
-
ORDER BY turn_index ASC
|
|
37
|
-
`)
|
|
38
|
-
.all(OLD_SESSION_KEY) as any[];
|
|
39
|
-
|
|
40
|
-
console.log(`[migrate] Found ${turns.length} turns under ${OLD_SESSION_KEY}`);
|
|
41
|
-
|
|
42
|
-
if (turns.length === 0) {
|
|
43
|
-
console.log("[migrate] Nothing to migrate.");
|
|
44
|
-
db.close();
|
|
45
|
-
process.exit(0);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Group by channelId from metadata
|
|
49
|
-
const byChannel = new Map<string, typeof turns>();
|
|
50
|
-
const noChannel: typeof turns = [];
|
|
51
|
-
|
|
52
|
-
for (const turn of turns) {
|
|
53
|
-
let channelId: string | null = null;
|
|
54
|
-
try {
|
|
55
|
-
const meta = JSON.parse(String((turn as any).metadata_json || "{}"));
|
|
56
|
-
channelId = meta.channelId || null;
|
|
57
|
-
} catch {}
|
|
58
|
-
|
|
59
|
-
if (!channelId) {
|
|
60
|
-
noChannel.push(turn);
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (!byChannel.has(channelId)) byChannel.set(channelId, []);
|
|
65
|
-
byChannel.get(channelId)!.push(turn);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
console.log(`[migrate] Splitting into ${byChannel.size} channels:`);
|
|
69
|
-
for (const [ch, chTurns] of byChannel) {
|
|
70
|
-
console.log(` discord:default:${ch} -> ${chTurns.length} turns`);
|
|
71
|
-
}
|
|
72
|
-
if (noChannel.length > 0) {
|
|
73
|
-
console.log(` (${noChannel.length} turns have no channelId -- left in old key)`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Find the current max turn_index for each target channel key
|
|
77
|
-
function getMaxTurnIndex(sessionKey: string): number {
|
|
78
|
-
const row = db
|
|
79
|
-
.prepare("SELECT MAX(turn_index) as max_idx FROM memory_turns WHERE session_key = ?")
|
|
80
|
-
.get(sessionKey) as any;
|
|
81
|
-
return row?.max_idx ?? 0;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Ensure per-channel session keys exist in memory_sessions
|
|
85
|
-
const upsertSession = db.prepare(`
|
|
86
|
-
INSERT OR IGNORE INTO memory_sessions (session_key, created_at)
|
|
87
|
-
VALUES (?, datetime('now'))
|
|
88
|
-
`);
|
|
89
|
-
|
|
90
|
-
// Update each turn's session_key and re-index
|
|
91
|
-
const updateTurn = db.prepare(`
|
|
92
|
-
UPDATE memory_turns SET session_key = ?, turn_index = ? WHERE id = ?
|
|
93
|
-
`);
|
|
94
|
-
|
|
95
|
-
// Copy runtime state for new keys
|
|
96
|
-
const readRuntimeState = db.prepare(`
|
|
97
|
-
SELECT * FROM memory_runtime_state WHERE session_key = ?
|
|
98
|
-
`);
|
|
99
|
-
const upsertRuntimeState = db.prepare(`
|
|
100
|
-
INSERT OR IGNORE INTO memory_runtime_state (session_key, runtime_context_id, runtime_epoch, updated_at)
|
|
101
|
-
VALUES (?, ?, ?, datetime('now'))
|
|
102
|
-
`);
|
|
103
|
-
|
|
104
|
-
db.exec("BEGIN TRANSACTION");
|
|
105
|
-
try {
|
|
106
|
-
const oldRuntime = readRuntimeState.get(OLD_SESSION_KEY) as any;
|
|
107
|
-
|
|
108
|
-
for (const [channelId, chTurns] of byChannel) {
|
|
109
|
-
const newKey = `discord:default:${channelId}`;
|
|
110
|
-
upsertSession.run(newKey);
|
|
111
|
-
|
|
112
|
-
// Start numbering after any existing turns in the target key
|
|
113
|
-
const startIndex = getMaxTurnIndex(newKey) + 1;
|
|
114
|
-
|
|
115
|
-
for (let i = 0; i < chTurns.length; i++) {
|
|
116
|
-
updateTurn.run(newKey, startIndex + i, chTurns[i].id);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Bootstrap runtime state for the new key if it doesn't exist
|
|
120
|
-
if (oldRuntime) {
|
|
121
|
-
upsertRuntimeState.run(newKey, `migrated_from_${OLD_SESSION_KEY}`, 1);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
console.log(
|
|
125
|
-
`[migrate] Updated ${chTurns.length} turns -> ${newKey} (indices ${startIndex}..${startIndex + chTurns.length - 1})`,
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
db.exec("COMMIT");
|
|
130
|
-
console.log("[migrate] Migration complete.");
|
|
131
|
-
} catch (err) {
|
|
132
|
-
db.exec("ROLLBACK");
|
|
133
|
-
console.error("[migrate] Migration failed, rolled back:", (err as Error).message || err);
|
|
134
|
-
process.exit(1);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Summary
|
|
138
|
-
for (const [channelId] of byChannel) {
|
|
139
|
-
const newKey = `discord:default:${channelId}`;
|
|
140
|
-
const count = (db.prepare("SELECT COUNT(*) as cnt FROM memory_turns WHERE session_key = ?").get(newKey) as any).cnt;
|
|
141
|
-
console.log(` ${newKey}: ${count} turns total`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const remaining = (
|
|
145
|
-
db.prepare("SELECT COUNT(*) as cnt FROM memory_turns WHERE session_key = ?").get(OLD_SESSION_KEY) as any
|
|
146
|
-
).cnt;
|
|
147
|
-
console.log(` ${OLD_SESSION_KEY}: ${remaining} turns remaining`);
|
|
148
|
-
|
|
149
|
-
db.close();
|