diff --git a/src/signal/daemon.ts b/src/signal/daemon.ts index ca1b01b60..56c7b584e 100644 --- a/src/signal/daemon.ts +++ b/src/signal/daemon.ts @@ -1,4 +1,7 @@ import { spawn } from "node:child_process"; +import { existsSync, mkdirSync, symlinkSync, lstatSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; import type { RuntimeEnv } from "../runtime.js"; export type SignalDaemonOpts = { @@ -18,6 +21,84 @@ export type SignalDaemonHandle = { stop: () => void; }; +/** + * Setup Signal data directory for containerized environments. + * Auto-detects persistent storage and creates symlink if needed. + * + * Priority order: + * 1. SIGNAL_CLI_DATA_DIR env var (if set) + * 2. /data/.local/share/signal-cli (Fly.io convention) + * 3. /var/lib/clawdbot/.local/share/signal-cli (alternative persistent location) + * 4. Default: ~/.local/share/signal-cli (no symlink needed) + */ +export function setupSignalDataPersistence(runtime?: RuntimeEnv): void { + const log = runtime?.log ?? (() => {}); + const defaultPath = join(homedir(), ".local", "share", "signal-cli"); + + // Check if already a symlink or regular directory exists + if (existsSync(defaultPath)) { + try { + const stats = lstatSync(defaultPath); + if (stats.isSymbolicLink()) { + log(`signal-cli data already symlinked to persistent storage`); + return; + } + // Regular directory exists, don't touch it + return; + } catch { + // Ignore errors, will try to create below + } + } + + // Determine persistent storage location + let persistentPath: string | null = null; + + // Priority 1: Environment variable + if (process.env.SIGNAL_CLI_DATA_DIR) { + persistentPath = process.env.SIGNAL_CLI_DATA_DIR; + log(`signal-cli: using SIGNAL_CLI_DATA_DIR=${persistentPath}`); + } + // Priority 2: /data (Fly.io convention) + else if (existsSync("/data")) { + persistentPath = "/data/.local/share/signal-cli"; + } + // Priority 3: /var/lib/clawdbot + else if (existsSync("/var/lib/clawdbot")) { + persistentPath = "/var/lib/clawdbot/.local/share/signal-cli"; + } + + // No persistent storage detected, use default + if (!persistentPath) { + return; + } + + // Create persistent directory structure + try { + mkdirSync(persistentPath, { recursive: true }); + mkdirSync(join(persistentPath, "data"), { recursive: true }); + } catch (err) { + runtime?.error?.(`signal-cli: failed to create persistent directory: ${String(err)}`); + return; + } + + // Create parent directory for symlink + try { + const parentDir = join(homedir(), ".local", "share"); + mkdirSync(parentDir, { recursive: true }); + } catch (err) { + runtime?.error?.(`signal-cli: failed to create parent directory: ${String(err)}`); + return; + } + + // Create symlink + try { + symlinkSync(persistentPath, defaultPath); + log(`signal-cli: linked ${defaultPath} -> ${persistentPath} for persistence`); + } catch (err) { + runtime?.error?.(`signal-cli: failed to create symlink: ${String(err)}`); + } +} + export function classifySignalCliLogLine(line: string): "log" | "error" | null { const trimmed = line.trim(); if (!trimmed) return null; @@ -48,6 +129,9 @@ function buildDaemonArgs(opts: SignalDaemonOpts): string[] { } export function spawnSignalDaemon(opts: SignalDaemonOpts): SignalDaemonHandle { + // Setup persistent storage for containerized environments + setupSignalDataPersistence(opts.runtime); + const args = buildDaemonArgs(opts); const child = spawn(opts.cliPath, args, { stdio: ["ignore", "pipe", "pipe"],