@hoverlover/cc-discord 0.2.3 → 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` (generated from `.claude/settings.template.json` by `bun run generate-settings`). The template uses `__ORCHESTRATOR_DIR__` placeholders that are replaced with absolute paths at generation time.
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",
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 .",
@@ -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
@@ -88,38 +88,8 @@ export AGENT_ID="$CHANNEL_ID"
88
88
  export CLAUDE_AGENT_ID="${CLAUDE_AGENT_ID:-claude-discord}"
89
89
  export CLAUDE_RUNTIME_ID="${CLAUDE_RUNTIME_ID:-rt_$(date +%s)_${RANDOM}}"
90
90
 
91
- # Determine Claude's project directory (where .claude/skills/ are discovered).
92
- # - CC_DISCORD_HOME overrides everything
93
- # - If cwd has a .claude/ directory (local dev / bun start from repo), use cwd
94
- # - Otherwise default to ~/.cc-discord (bunx / installed package)
95
- if [ -n "${CC_DISCORD_HOME:-}" ]; then
96
- CLAUDE_PROJECT_DIR="$CC_DISCORD_HOME"
97
- elif [ -d ".claude" ]; then
98
- CLAUDE_PROJECT_DIR="$(pwd)"
99
- else
100
- CLAUDE_PROJECT_DIR="$HOME/.cc-discord"
101
- fi
102
-
103
- mkdir -p "$CLAUDE_PROJECT_DIR/.claude/skills"
104
-
105
- # Seed built-in skills from the package into the project directory.
106
- # Only copies skills that don't already exist (user modifications are preserved).
107
- if [ -d "$ROOT_DIR/.claude/skills" ] && [ "$ROOT_DIR" != "$CLAUDE_PROJECT_DIR" ]; then
108
- for skill_dir in "$ROOT_DIR/.claude/skills"/*/; do
109
- skill_name="$(basename "$skill_dir")"
110
- if [ ! -d "$CLAUDE_PROJECT_DIR/.claude/skills/$skill_name" ]; then
111
- cp -r "$skill_dir" "$CLAUDE_PROJECT_DIR/.claude/skills/$skill_name"
112
- echo "[channel-agent:$CHANNEL_NAME] Seeded skill: $skill_name"
113
- fi
114
- done
115
- fi
116
-
117
- # Copy settings.json into the project dir so Claude sees it as a project config.
118
- if [ "$ROOT_DIR" != "$CLAUDE_PROJECT_DIR" ] && [ -f "$SETTINGS_PATH" ]; then
119
- mkdir -p "$CLAUDE_PROJECT_DIR/.claude"
120
- cp "$SETTINGS_PATH" "$CLAUDE_PROJECT_DIR/.claude/settings.json"
121
- fi
122
-
91
+ # Project directory inherited from start.sh, or derived here for standalone use.
92
+ CLAUDE_PROJECT_DIR="${CC_DISCORD_HOME:-$HOME/.cc-discord}"
123
93
  cd "$CLAUDE_PROJECT_DIR"
124
94
  echo "[channel-agent:$CHANNEL_NAME] Claude project dir: $CLAUDE_PROJECT_DIR"
125
95
 
@@ -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
@@ -69,6 +69,30 @@ if $log_setup; then
69
69
  echo "[start] Edit .env.relay and .env.worker in that directory, then restart."
70
70
  fi
71
71
 
72
+ # Project directory for Claude (skills, settings). Exported for channel-agent.sh.
73
+ export CC_DISCORD_HOME="${CC_DISCORD_HOME:-$HOME/.cc-discord}"
74
+ mkdir -p "$CC_DISCORD_HOME/.claude/skills"
75
+
76
+ # Seed built-in skills from the package into the project directory.
77
+ if [ -d "$ROOT_DIR/.claude/skills" ] && [ "$ROOT_DIR" != "$CC_DISCORD_HOME" ]; then
78
+ for skill_dir in "$ROOT_DIR/.claude/skills"/*/; do
79
+ [ -d "$skill_dir" ] || continue
80
+ skill_name="$(basename "$skill_dir")"
81
+ if [ ! -d "$CC_DISCORD_HOME/.claude/skills/$skill_name" ]; then
82
+ cp -r "$skill_dir" "$CC_DISCORD_HOME/.claude/skills/$skill_name"
83
+ echo "[start] Seeded skill: $skill_name"
84
+ fi
85
+ done
86
+ fi
87
+
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
+ bash "$ROOT_DIR/scripts/generate-settings.sh"
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
+ fi
94
+ fi
95
+
72
96
  # Log directory (shared with orchestrator and channel agents)
73
97
  export CC_DISCORD_LOG_DIR="${CC_DISCORD_LOG_DIR:-/tmp/cc-discord/logs}"
74
98
  mkdir -p "$CC_DISCORD_LOG_DIR"
@@ -96,6 +120,10 @@ cleanup() {
96
120
  wait "$RELAY_PID" 2>/dev/null || true
97
121
  fi
98
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
+
99
127
  log "All processes stopped."
100
128
  exit 0
101
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();