import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join, resolve } from "node:path"; export const DEFAULT_PORT = 37849; export const DEFAULT_BIND = "127.0.0.1"; export const DEFAULT_HIDE_AFTER_MINUTES = 60 * 24; export const DEFAULT_DELETE_AFTER_MINUTES = 60 * 24 * 7; export const DEFAULT_SWEEP_INTERVAL_MINUTES = 60; export interface FileConfig { port?: number; bind?: string; dbPath?: string; hideAfterMinutes?: number; deleteAfterMinutes?: number; sweepIntervalMinutes?: number; } export interface DaemonConfig { port: number; bind: string; dbPath: string; hideAfterMinutes: number; deleteAfterMinutes: number; sweepIntervalMinutes: number; } export function defaultDbPath(): string { return join(homedir(), ".claude-mailbox", "mailbox.db"); } export function userConfigPath(): string { return join(homedir(), ".claude-mailbox", "mailbox.json"); } export function machineConfigPath(): string | null { if (process.platform === "win32") { const programData = process.env["ProgramData"] ?? "C:\\ProgramData"; return join(programData, "ClaudeMailbox", "mailbox.json"); } if (process.platform === "darwin") { return "/Library/Application Support/ClaudeMailbox/mailbox.json"; } return "/etc/claude-mailbox/mailbox.json"; } function expandPath(p: string): string { let out = p; if (out.startsWith("~")) out = join(homedir(), out.slice(1)); out = out.replace(/%([^%]+)%/g, (_, name) => process.env[name] ?? ""); out = out.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, name) => process.env[name] ?? ""); return resolve(out); } export function loadFileConfig(explicitPath?: string): FileConfig { const candidates: string[] = []; if (explicitPath) { if (!existsSync(explicitPath)) { throw new Error(`Config file not found: ${explicitPath}`); } candidates.push(explicitPath); } else { candidates.push(userConfigPath()); const machine = machineConfigPath(); if (machine) candidates.push(machine); } for (const path of candidates) { if (existsSync(path)) { const raw = readFileSync(path, "utf8"); const parsed = JSON.parse(raw) as FileConfig; return { port: typeof parsed.port === "number" ? parsed.port : undefined, bind: typeof parsed.bind === "string" ? parsed.bind : undefined, dbPath: typeof parsed.dbPath === "string" ? parsed.dbPath : undefined, hideAfterMinutes: typeof parsed.hideAfterMinutes === "number" ? parsed.hideAfterMinutes : undefined, deleteAfterMinutes: typeof parsed.deleteAfterMinutes === "number" ? parsed.deleteAfterMinutes : undefined, sweepIntervalMinutes: typeof parsed.sweepIntervalMinutes === "number" ? parsed.sweepIntervalMinutes : undefined, }; } } return {}; } export interface ServeOverrides { port?: number; bind?: string; dbPath?: string; config?: string; hideAfterMinutes?: number; deleteAfterMinutes?: number; sweepIntervalMinutes?: number; } export function resolveConfig(overrides: ServeOverrides): DaemonConfig { const file = loadFileConfig(overrides.config); const port = overrides.port ?? file.port ?? DEFAULT_PORT; const bind = overrides.bind ?? file.bind ?? DEFAULT_BIND; const dbPathRaw = overrides.dbPath ?? file.dbPath ?? defaultDbPath(); const hideAfterMinutes = overrides.hideAfterMinutes ?? file.hideAfterMinutes ?? DEFAULT_HIDE_AFTER_MINUTES; const deleteAfterMinutes = overrides.deleteAfterMinutes ?? file.deleteAfterMinutes ?? DEFAULT_DELETE_AFTER_MINUTES; const sweepIntervalMinutes = overrides.sweepIntervalMinutes ?? file.sweepIntervalMinutes ?? DEFAULT_SWEEP_INTERVAL_MINUTES; return { port, bind, dbPath: expandPath(dbPathRaw), hideAfterMinutes, deleteAfterMinutes, sweepIntervalMinutes, }; } export function baseUrl(cfg: { port: number; bind: string }): string { return `http://${cfg.bind}:${cfg.port}`; }