@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 +154 -4
- package/package.json +1 -3
- package/scripts/channel-agent.sh +3 -33
- package/scripts/generate-settings.sh +1 -1
- package/scripts/start-orchestrator.sh +1 -1
- package/scripts/start.sh +28 -0
- package/scripts/migrate-memory-to-channel-keys.ts +0 -149
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
|
|
@@ -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
|
-
#
|
|
92
|
-
|
|
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();
|