import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs"; import { dirname, join } from "node:path"; import { run, cliEntry, type AutostartManager, type AutostartInstallOpts } from "./index.js"; import { userConfigPath } from "../config.js"; function markerPath(): string { return join(dirname(userConfigPath()), MARKER_FILE); } function readActiveMode(): "task" | "run-key" | null { const path = markerPath(); if (!existsSync(path)) return null; const raw = readFileSync(path, "utf8").trim(); if (raw === "task" || raw === "run-key") return raw; return null; } function writeActiveMode(mode: "task" | "run-key"): void { const path = markerPath(); mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, mode, "utf8"); } function clearActiveMode(): void { const path = markerPath(); if (existsSync(path)) rmSync(path, { force: true }); } function isAccessDenied(stderr: string): boolean { return /access is denied|0x80070005/i.test(stderr); } const TASK_NAME = "ClaudeMailbox"; const SERVICE_NAME = "ClaudeMailbox"; const RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"; const RUN_VALUE = "ClaudeMailbox"; const MARKER_FILE = "autostart-mode"; function ensureConfigSeeded(opts: AutostartInstallOpts): string { const path = userConfigPath(); if (!existsSync(path)) { mkdirSync(join(path, ".."), { recursive: true }); const seed: Record = {}; if (opts.port !== undefined) seed.port = opts.port; if (opts.bind !== undefined) seed.bind = opts.bind; if (opts.dbPath !== undefined) seed.dbPath = opts.dbPath; writeFileSync(path, JSON.stringify(seed, null, 2) + "\n", "utf8"); } return path; } function buildServeCommand(): { node: string; script: string; configPath: string } { const { node, script } = cliEntry(); return { node, script, configPath: userConfigPath() }; } function buildServeCommandString(configPath: string): string { const { node, script } = buildServeCommand(); return `"${node}" "${script}" serve --config "${configPath}"`; } function tryScheduledTaskInstall(opts: AutostartInstallOpts): { ok: boolean; stderr: string } { const configPath = ensureConfigSeeded(opts); const tr = buildServeCommandString(configPath); const r = run("schtasks.exe", [ "/Create", "/SC", "ONLOGON", "/TN", TASK_NAME, "/TR", tr, "/RL", "LIMITED", "/F", ]); if (r.status !== 0) return { ok: false, stderr: r.stderr || r.stdout }; run("schtasks.exe", ["/Run", "/TN", TASK_NAME]); return { ok: true, stderr: "" }; } function runKeyInstall(opts: AutostartInstallOpts): void { const configPath = ensureConfigSeeded(opts); const cmd = buildServeCommandString(configPath); const r = run("reg.exe", [ "add", RUN_KEY, "/v", RUN_VALUE, "/t", "REG_SZ", "/d", cmd, "/f", ]); if (r.status !== 0) { throw new Error(`reg add (HKCU Run) failed (exit ${r.status}): ${r.stderr || r.stdout}`); } spawnRunKeyDaemon(configPath); } function spawnRunKeyDaemon(configPath: string): void { if (runKeyDaemonRunning()) return; const { node, script } = buildServeCommand(); const ps = `Start-Process -WindowStyle Hidden -FilePath "${node}" -ArgumentList @('"${script}"','serve','--config','"${configPath}"')`; run("powershell.exe", ["-NoProfile", "-Command", ps]); } function runKeyDaemonRunning(): boolean { const ps = `Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Where-Object { $_.CommandLine -like '*claude-mailbox*serve*' } | Select-Object -First 1 -ExpandProperty ProcessId`; const r = run("powershell.exe", ["-NoProfile", "-Command", ps]); return r.status === 0 && r.stdout.trim().length > 0; } function killRunKeyDaemon(): void { const ps = `Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Where-Object { $_.CommandLine -like '*claude-mailbox*serve*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }`; run("powershell.exe", ["-NoProfile", "-Command", ps]); } function runKeyUninstall(): void { const r = run("reg.exe", ["delete", RUN_KEY, "/v", RUN_VALUE, "/f"]); if (r.status !== 0 && !/unable to find/i.test(r.stderr)) { throw new Error(`reg delete failed (exit ${r.status}): ${r.stderr || r.stdout}`); } killRunKeyDaemon(); } function scheduledTaskInstall(opts: AutostartInstallOpts): void { const attempt = tryScheduledTaskInstall(opts); if (attempt.ok) { writeActiveMode("task"); return; } if (isAccessDenied(attempt.stderr)) { console.warn( "schtasks /Create denied by Windows policy — falling back to HKCU Run-key autostart (per-user, no admin).", ); runKeyInstall(opts); writeActiveMode("run-key"); return; } throw new Error(`schtasks /Create failed: ${attempt.stderr}`); } function scheduledTaskUninstall(purge: boolean): void { const mode = readActiveMode(); if (mode === "run-key") { runKeyUninstall(); clearActiveMode(); if (purge) purgeData(); return; } // Default to task uninstall, also clean up Run-key in case of mixed state run("schtasks.exe", ["/End", "/TN", TASK_NAME]); const r = run("schtasks.exe", ["/Delete", "/TN", TASK_NAME, "/F"]); if (r.status !== 0 && !/cannot find/i.test(r.stderr) && !/does not exist/i.test(r.stderr)) { // Fall through — try Run-key cleanup anyway } // Best-effort Run-key cleanup run("reg.exe", ["delete", RUN_KEY, "/v", RUN_VALUE, "/f"]); killRunKeyDaemon(); clearActiveMode(); if (purge) purgeData(); } function scheduledTaskStatus(): "Running" | "Stopped" | "NotInstalled" { const mode = readActiveMode(); if (mode === "run-key") { const r = run("reg.exe", ["query", RUN_KEY, "/v", RUN_VALUE]); if (r.status !== 0) return "NotInstalled"; return runKeyDaemonRunning() ? "Running" : "Stopped"; } const r = run("schtasks.exe", ["/Query", "/TN", TASK_NAME, "/FO", "LIST", "/V"]); if (r.status !== 0) { if (/cannot find/i.test(r.stderr) || /does not exist/i.test(r.stderr)) { // Maybe a Run-key install happened without a marker (legacy / manual). Check reg. const reg = run("reg.exe", ["query", RUN_KEY, "/v", RUN_VALUE]); if (reg.status === 0) return runKeyDaemonRunning() ? "Running" : "Stopped"; return "NotInstalled"; } return "Stopped"; } if (/Status:\s*Running/i.test(r.stdout)) return "Running"; return "Stopped"; } function scheduledTaskRun(): void { const mode = readActiveMode(); if (mode === "run-key") { const cfgPath = userConfigPath(); spawnRunKeyDaemon(cfgPath); return; } const r = run("schtasks.exe", ["/Run", "/TN", TASK_NAME]); if (r.status !== 0) throw new Error(`schtasks /Run failed: ${r.stderr || r.stdout}`); } function scheduledTaskEnd(): void { const mode = readActiveMode(); if (mode === "run-key") { killRunKeyDaemon(); return; } run("schtasks.exe", ["/End", "/TN", TASK_NAME]); } interface NodeWindowsService { on(event: "install" | "uninstall" | "alreadyinstalled" | "start" | "stop", cb: () => void): void; install(): void; uninstall(): void; start(): void; stop(): void; exists?: boolean; } interface NodeWindowsModule { Service: new (opts: { name: string; description?: string; script: string; nodeOptions?: string[]; workingDirectory?: string; }) => NodeWindowsService; } async function loadNodeWindows(): Promise { try { return (await import("node-windows")) as unknown as NodeWindowsModule; } catch (err) { throw new Error( "node-windows is not installed. Install it with `npm i -g node-windows` or use the default Scheduled Task autostart instead.", ); } } function isAdministrator(): boolean { const r = run("net.exe", ["session"]); return r.status === 0; } async function serviceInstall(opts: AutostartInstallOpts): Promise { if (!isAdministrator()) { throw new Error("install-autostart --service requires an Administrator shell."); } ensureConfigSeeded(opts); const { script, configPath } = buildServeCommand(); const nw = await loadNodeWindows(); await new Promise((resolveFn, rejectFn) => { const svc = new nw.Service({ name: SERVICE_NAME, description: "ClaudeMailbox MCP mail daemon for parallel Claude session coordination.", script, nodeOptions: [], }); svc.on("install", () => { svc.start(); resolveFn(); }); svc.on("alreadyinstalled", () => resolveFn()); try { svc.install(); } catch (e) { rejectFn(e); } void configPath; }); } async function serviceUninstall(purge: boolean): Promise { if (!isAdministrator()) { throw new Error("uninstall-autostart --service requires an Administrator shell."); } const { script } = buildServeCommand(); const nw = await loadNodeWindows(); await new Promise((resolveFn, rejectFn) => { const svc = new nw.Service({ name: SERVICE_NAME, script }); svc.on("uninstall", () => resolveFn()); try { svc.uninstall(); } catch (e) { rejectFn(e); } }); if (purge) purgeData(); } function serviceStatus(): "Running" | "Stopped" | "NotInstalled" { const r = run("sc.exe", ["query", SERVICE_NAME]); if (r.status !== 0) return "NotInstalled"; if (/STATE\s*:\s*\d+\s+RUNNING/i.test(r.stdout)) return "Running"; return "Stopped"; } function serviceStart(): void { const r = run("sc.exe", ["start", SERVICE_NAME]); if (r.status !== 0) throw new Error(`sc start failed: ${r.stderr || r.stdout}`); } function serviceStop(): void { const r = run("sc.exe", ["stop", SERVICE_NAME]); if (r.status !== 0 && !/has not been started/i.test(r.stdout)) { throw new Error(`sc stop failed: ${r.stderr || r.stdout}`); } } function purgeData(): void { const cfg = userConfigPath(); if (existsSync(cfg)) { try { const parsed = JSON.parse(readFileSync(cfg, "utf8")) as { dbPath?: string }; void parsed; } catch { // ignore } } } export function windowsManager(mode: "default" | "service"): AutostartManager { if (mode === "service") { return { mode, install: serviceInstall, uninstall: serviceUninstall, async start() { serviceStart(); }, async stop() { serviceStop(); }, async status() { return serviceStatus(); }, }; } return { mode, async install(opts) { scheduledTaskInstall(opts); }, async uninstall(purge) { scheduledTaskUninstall(purge); }, async start() { scheduledTaskRun(); }, async stop() { scheduledTaskEnd(); }, async status() { return scheduledTaskStatus(); }, }; }