#!/usr/bin/env node // scheduler.mjs — Spor C / C1 Tier 2: install/manage the local launchd // LaunchAgent that runs the Claude-FREE detection pipeline on a daily schedule, // independent of Claude sessions = real background BETWEEN sessions. // // STRUCTURAL ToS GUARANTEE: this script only ever spawns the running node // binary (process.execPath) on the allow-listed run-detection.mjs, and the // `launchctl` CLI for agent management. It can NEVER invoke `claude`. The // scheduled entrypoint (`run`) gates on cadence, then delegates to // run-detection.mjs, which is itself structurally Claude-free. Apply (the only // Claude step) stays manual + in-session + gated. See detection-schedule.mjs // for the ToS rationale (Consumer Terms §3.7 targets Anthropic's services, not // local scripts). // // node scripts/kb-update/scheduler.mjs install # write plist + load agent // node scripts/kb-update/scheduler.mjs uninstall # unload + remove plist // node scripts/kb-update/scheduler.mjs status # is it loaded? // node scripts/kb-update/scheduler.mjs run-now # trigger one run now // node scripts/kb-update/scheduler.mjs print # show the plist (alias --dry-run) // node scripts/kb-update/scheduler.mjs run # (the launchd entrypoint) import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { homedir } from 'node:os'; import { existsSync, readFileSync, mkdirSync, rmSync } from 'node:fs'; import { execFileSync } from 'node:child_process'; import { renderPlist, resolvePaths, defaultLabel } from './lib/launchd-plist.mjs'; import { loadScheduleConfig, daysSinceLastPoll, } from './lib/detection-schedule.mjs'; import { atomicWriteSync } from './lib/atomic-write.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const PLUGIN_ROOT = join(__dirname, '..', '..'); const RUN_DETECTION = join(PLUGIN_ROOT, 'scripts', 'kb-update', 'run-detection.mjs'); const CHANGE_REPORT = join(PLUGIN_ROOT, 'scripts', 'kb-update', 'data', 'change-report.json'); // Daily fire time. Not configurable yet (onboarding/C2 may expose it). const FIRE_HOUR = 3; const FIRE_MINUTE = 0; /** Build renderPlist inputs from the resolved paths + the running node. */ function planAgent() { const nodePath = process.execPath; const paths = resolvePaths({ pluginRoot: PLUGIN_ROOT, homeDir: homedir(), nodePath }); const plist = renderPlist({ label: paths.label, nodePath, scriptPath: paths.scriptPath, args: ['run'], hour: FIRE_HOUR, minute: FIRE_MINUTE, logPath: paths.logPath, workingDir: paths.workingDir, }); return { ...paths, nodePath, plist }; } /** gui/ domain + gui//