Chronicle Visualizer local viewer server
Imported JavaScript file from local-tools/chronicle-visualizer/server.js.
Source file
local-tools/chronicle-visualizer/server.js
JavaScript387 lines
import crypto from "node:crypto";import fs from "node:fs";import http from "node:http";import os from "node:os";import path from "node:path";import { execFileSync } from "node:child_process";import { fileURLToPath } from "node:url";const __filename = fileURLToPath(import.meta.url);const __dirname = path.dirname(__filename);const PORT = Number(process.env.PORT || 4177);const SCREEN_ROOT =process.env.CHRONICLE_SCREEN_ROOT ||path.join(os.tmpdir(), "chronicle", "screen_recording");const MEMORY_ROOT =process.env.CHRONICLE_MEMORY_ROOT ||path.join(os.homedir(), ".codex", "memories", "extensions", "chronicle", "resources");const PID_PATH = path.join(os.tmpdir(), "codex_chronicle", "chronicle-started.pid");const CACHE_ROOT = path.join(__dirname, ".cache");const THUMB_ROOT = path.join(CACHE_ROOT, "thumbnails");const PUBLIC_ROOT = path.join(__dirname, "public");const PROJECT_RULES = [["hyphenomenon", /\bhyphenomenon\b/i],["rps-etsy", /\b(rps[-_\s]?etsy|rock paper scissors|rps catalog|rps creative)\b/i],["family-shapes", /\b(family[-_\s]?shapes|donor conception|donor[-_\s]?gamete)\b/i],["codex-skills", /\b(codex[-_\s]?skills|skills marketplace|skill library|plugin package)\b/i],["tmora", /\b(tmora|print center|photoshop|proof deck|postcard)\b/i],["shopify", /\b(shopify|hydrogen|digital downloads|metafield)\b/i],["gmail", /\b(gmail|support email|inbox|shipment email)\b/i],["etsy", /\b(etsy|shop manager|listing media|seller)\b/i],["maggie-todo", /\b(maggie todo|intake queue|iq-\d+)\b/i],["openai", /\b(openai|agent builder|chat prompts|responses api|gpt[-_\s]?\d|sora)\b/i],["google-drive", /\b(google drive|google sheets|google docs|drive migration)\b/i],["vercel", /\bvercel\b/i],["netlify", /\bnetlify\b/i]];let indexCache = null;let indexCacheAt = 0;function ensureDir(dir) {fs.mkdirSync(dir, { recursive: true });}function hashId(value) {return crypto.createHash("sha1").update(value).digest("hex").slice(0, 16);}function walkFiles(root, predicate, out = []) {if (!fs.existsSync(root)) return out;for (const entry of fs.readdirSync(root, { withFileTypes: true })) {const fullPath = path.join(root, entry.name);if (entry.isDirectory()) {walkFiles(fullPath, predicate, out);} else if (!predicate || predicate(fullPath)) {out.push(fullPath);}}return out;}function statMtimeMs(filePath) {try {return fs.statSync(filePath).mtimeMs;} catch {return 0;}}function chronicleStatus() {let running = false;let latestFrameAt = null;let latestFrameAgeSeconds = null;try {const pid = Number(fs.readFileSync(PID_PATH, "utf8").trim());process.kill(pid, 0);running = true;} catch {running = false;}const latestFrames = walkFiles(SCREEN_ROOT, (filePath) => filePath.endsWith("-latest.jpg"));let newestMtime = 0;for (const framePath of latestFrames) {newestMtime = Math.max(newestMtime, statMtimeMs(framePath));}if (newestMtime) {latestFrameAt = new Date(newestMtime).toISOString();latestFrameAgeSeconds = Math.max(0, Math.round((Date.now() - newestMtime) / 1000));}return { running, latestFrameAt, latestFrameAgeSeconds };}function parseFrameTimestamp(filePath) {const fileName = path.basename(filePath);const minuteMatch = fileName.match(/frame-\d+-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z)\.jpg$/);if (minuteMatch) return parseChronicleTimestamp(minuteMatch[1]);const latestMatch = fileName.match(/^(.+)-display-\d+-latest\.jpg$/);if (latestMatch) return parseChronicleTimestamp(latestMatch[1]);return new Date(statMtimeMs(filePath)).toISOString();}function parseChronicleTimestamp(raw) {const normalized = raw.replace(/T(\d{2})-(\d{2})-(\d{2})/, "T$1:$2:$3").replace(/\+00-00$/, "Z");const parsed = new Date(normalized);return Number.isNaN(parsed.getTime()) ? new Date().toISOString() : parsed.toISOString();}function parseSegmentKey(filePath) {const dirName = path.basename(path.dirname(filePath));if (dirName.includes("-display-")) return dirName;const fileName = path.basename(filePath);const latest = fileName.match(/^(.+-display-\d+)-latest\.jpg$/);if (latest) return latest[1];const sidecar = fileName.match(/^(.+-display-\d+)\.(ocr\.jsonl|capture|capture\.json)$/);if (sidecar) return sidecar[1];return "";}function parseDisplay(filePath) {const match = filePath.match(/display-(\d+)/);return match ? match[1] : "unknown";}function extractStrings(value, out = []) {if (typeof value === "string") {if (value.trim()) out.push(value.trim());return out;}if (Array.isArray(value)) {for (const item of value) extractStrings(item, out);return out;}if (value && typeof value === "object") {for (const item of Object.values(value)) extractStrings(item, out);}return out;}function loadOcrBySegment() {const sidecars = walkFiles(SCREEN_ROOT, (filePath) => filePath.endsWith(".ocr.jsonl"));const bySegment = new Map();for (const sidecarPath of sidecars) {const segmentKey = parseSegmentKey(sidecarPath);const snippets = [];try {const lines = fs.readFileSync(sidecarPath, "utf8").split(/\r?\n/).filter(Boolean);for (const line of lines.slice(-250)) {try {const strings = extractStrings(JSON.parse(line));snippets.push(...strings);} catch {snippets.push(line);}}} catch {continue;}const text = Array.from(new Set(snippets)).join(" ").replace(/\s+/g, " ").slice(0, 20000);bySegment.set(segmentKey, { sidecarPath, text });}return bySegment;}function loadMemories() {const files = walkFiles(MEMORY_ROOT, (filePath) => filePath.endsWith(".md"));return files.map((filePath) => {const fileName = path.basename(filePath);const stamp = fileName.match(/^(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})/);const timestamp = stamp ? parseChronicleTimestamp(`${stamp[1]}Z`) : new Date(statMtimeMs(filePath)).toISOString();let text = "";try {text = fs.readFileSync(filePath, "utf8");} catch {text = "";}const cwdMatch = text.match(/\bcwd=([^\n,)]+)/i) || text.match(/\bcwd:\s*([^\n]+)/i);const kind = fileName.includes("-6h-") ? "6h" : "10min";return {id: hashId(filePath),fileName,path: filePath,timestamp,timeMs: new Date(timestamp).getTime(),kind,cwd: cwdMatch ? cwdMatch[1].trim() : "",text,excerpt: text.replace(/^---[\s\S]*?---/, "").replace(/\s+/g, " ").trim().slice(0, 900)};}).sort((a, b) => a.timeMs - b.timeMs);}function nearestMemory(frameMs, memories) {let best = null;let bestDelta = Infinity;for (const memory of memories) {const delta = Math.abs(memory.timeMs - frameMs);const windowMs = memory.kind === "6h" ? 6 * 60 * 60 * 1000 : 16 * 60 * 1000;if (delta <= windowMs && delta < bestDelta) {best = memory;bestDelta = delta;}}return best;}function inferredProjects(text) {const matches = [];for (const [key, rule] of PROJECT_RULES) {if (rule.test(text)) matches.push(key);}return matches;}function thumbPathFor(item) {return path.join(THUMB_ROOT, `${item.id}.jpg`);}function ensureThumbnail(item) {ensureDir(THUMB_ROOT);const destination = thumbPathFor(item);const sourceMtime = statMtimeMs(item.path);const thumbMtime = statMtimeMs(destination);if (thumbMtime && thumbMtime >= sourceMtime) return destination;try {execFileSync("sips", ["-Z", "320", item.path, "--out", destination], {stdio: "ignore"});} catch {fs.copyFileSync(item.path, destination);}return destination;}function frameFiles() {const latest = walkFiles(SCREEN_ROOT, (filePath) => filePath.endsWith("-latest.jpg"));const historical = walkFiles(path.join(SCREEN_ROOT, "1min"), (filePath) => filePath.endsWith(".jpg"));return [...historical, ...latest];}function buildIndex({ withThumbs = false } = {}) {const status = chronicleStatus();const ocrBySegment = loadOcrBySegment();const memories = loadMemories();const frames = frameFiles();const items = frames.map((filePath) => {const timestamp = parseFrameTimestamp(filePath);const timeMs = new Date(timestamp).getTime();const segmentKey = parseSegmentKey(filePath);const ocr = ocrBySegment.get(segmentKey) || null;const memory = nearestMemory(timeMs, memories);const id = hashId(filePath);const fileName = path.basename(filePath);const haystack = [fileName,filePath,segmentKey,ocr?.text || "",memory?.fileName || "",memory?.cwd || "",memory?.text || ""].join("\n");const item = {id,fileName,path: filePath,timestamp,timeMs,day: timestamp.slice(0, 10),display: parseDisplay(filePath),segmentKey,isLatest: fileName.endsWith("-latest.jpg"),thumbUrl: `/thumb/${id}.jpg`,frameUrl: `/frame/${id}.jpg`,ocrSidecarPath: ocr?.sidecarPath || "",ocrHint: ocr?.text ? ocr.text.slice(0, 900) : "",memory: memory? {id: memory.id,fileName: memory.fileName,path: memory.path,timestamp: memory.timestamp,kind: memory.kind,cwd: memory.cwd,excerpt: memory.excerpt}: null,projects: inferredProjects(haystack),searchText: haystack.toLowerCase().slice(0, 50000)};if (withThumbs) ensureThumbnail(item);return item;}).sort((a, b) => b.timeMs - a.timeMs);const projectCounts = {};for (const item of items) {for (const project of item.projects) {projectCounts[project] = (projectCounts[project] || 0) + 1;}}return {generatedAt: new Date().toISOString(),screenRoot: SCREEN_ROOT,memoryRoot: MEMORY_ROOT,cacheRoot: CACHE_ROOT,status,counts: {frames: items.length,memories: memories.length,ocrSidecars: ocrBySegment.size},projectCounts,items};}function getIndex(force = false) {const now = Date.now();if (!force && indexCache && now - indexCacheAt < 15000) return indexCache;indexCache = buildIndex();indexCacheAt = now;return indexCache;}function itemById(id) {const index = getIndex();return index.items.find((item) => item.id === id);}function sendJson(res, payload, statusCode = 200) {const body = JSON.stringify(payload);res.writeHead(statusCode, {"content-type": "application/json; charset=utf-8","cache-control": "no-store","content-length": Buffer.byteLength(body)});res.end(body);}function sendFile(res, filePath, contentType = "application/octet-stream") {if (!fs.existsSync(filePath)) {res.writeHead(404);res.end("Not found");return;}res.writeHead(200, {"content-type": contentType,"cache-control": "no-store"});fs.createReadStream(filePath).pipe(res);}function contentTypeFor(filePath) {if (filePath.endsWith(".html")) return "text/html; charset=utf-8";if (filePath.endsWith(".css")) return "text/css; charset=utf-8";if (filePath.endsWith(".js")) return "text/javascript; charset=utf-8";if (filePath.endsWith(".jpg") || filePath.endsWith(".jpeg")) return "image/jpeg";if (filePath.endsWith(".png")) return "image/png";if (filePath.endsWith(".svg")) return "image/svg+xml";return "application/octet-stream";}function serveStatic(req, res, url) {const requested = url.pathname === "/" ? "/index.html" : url.pathname;const normalized = path.normal...[truncated for intake]