Files
ClaudeMailbox/node/src/config.ts
Mika Kuns 0c06e2cf4b feat(cleanup): hide and prune stale mailboxes
Mailbox listings grew unbounded as old sessions ended without
unregistering. This adds two layers of cleanup, configurable via
mailbox.json or `serve` flags:

- Lazy filter: list responses (REST /v1/list, MCP list_mailboxes)
  drop mailboxes idle longer than hideAfterMinutes (default 24h),
  while always keeping the caller and any sender with messages
  pending for them.
- Background sweep: startServer runs an initial prune on boot and
  schedules an unref'd interval timer that hard-deletes mailboxes
  idle longer than deleteAfterMinutes (default 7d) which have no
  pending messages, and wipes their delivered history.
2026-05-20 13:54:03 +02:00

123 lines
3.9 KiB
TypeScript

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}`;
}