Code
    Chronicle VisualizerUpdated Jun 4, 2026JS

    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
    1. import crypto from "node:crypto";
    2. import fs from "node:fs";
    3. import http from "node:http";
    4. import os from "node:os";
    5. import path from "node:path";
    6. import { execFileSync } from "node:child_process";
    7. import { fileURLToPath } from "node:url";
    8. const __filename = fileURLToPath(import.meta.url);
    9. const __dirname = path.dirname(__filename);
    10. const PORT = Number(process.env.PORT || 4177);
    11. const SCREEN_ROOT =
    12. process.env.CHRONICLE_SCREEN_ROOT ||
    13. path.join(os.tmpdir(), "chronicle", "screen_recording");
    14. const MEMORY_ROOT =
    15. process.env.CHRONICLE_MEMORY_ROOT ||
    16. path.join(os.homedir(), ".codex", "memories", "extensions", "chronicle", "resources");
    17. const PID_PATH = path.join(os.tmpdir(), "codex_chronicle", "chronicle-started.pid");
    18. const CACHE_ROOT = path.join(__dirname, ".cache");
    19. const THUMB_ROOT = path.join(CACHE_ROOT, "thumbnails");
    20. const PUBLIC_ROOT = path.join(__dirname, "public");
    21. const PROJECT_RULES = [
    22. ["hyphenomenon", /\bhyphenomenon\b/i],
    23. ["rps-etsy", /\b(rps[-_\s]?etsy|rock paper scissors|rps catalog|rps creative)\b/i],
    24. ["family-shapes", /\b(family[-_\s]?shapes|donor conception|donor[-_\s]?gamete)\b/i],
    25. ["codex-skills", /\b(codex[-_\s]?skills|skills marketplace|skill library|plugin package)\b/i],
    26. ["tmora", /\b(tmora|print center|photoshop|proof deck|postcard)\b/i],
    27. ["shopify", /\b(shopify|hydrogen|digital downloads|metafield)\b/i],
    28. ["gmail", /\b(gmail|support email|inbox|shipment email)\b/i],
    29. ["etsy", /\b(etsy|shop manager|listing media|seller)\b/i],
    30. ["maggie-todo", /\b(maggie todo|intake queue|iq-\d+)\b/i],
    31. ["openai", /\b(openai|agent builder|chat prompts|responses api|gpt[-_\s]?\d|sora)\b/i],
    32. ["google-drive", /\b(google drive|google sheets|google docs|drive migration)\b/i],
    33. ["vercel", /\bvercel\b/i],
    34. ["netlify", /\bnetlify\b/i]
    35. ];
    36. let indexCache = null;
    37. let indexCacheAt = 0;
    38. function ensureDir(dir) {
    39. fs.mkdirSync(dir, { recursive: true });
    40. }
    41. function hashId(value) {
    42. return crypto.createHash("sha1").update(value).digest("hex").slice(0, 16);
    43. }
    44. function walkFiles(root, predicate, out = []) {
    45. if (!fs.existsSync(root)) return out;
    46. for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
    47. const fullPath = path.join(root, entry.name);
    48. if (entry.isDirectory()) {
    49. walkFiles(fullPath, predicate, out);
    50. } else if (!predicate || predicate(fullPath)) {
    51. out.push(fullPath);
    52. }
    53. }
    54. return out;
    55. }
    56. function statMtimeMs(filePath) {
    57. try {
    58. return fs.statSync(filePath).mtimeMs;
    59. } catch {
    60. return 0;
    61. }
    62. }
    63. function chronicleStatus() {
    64. let running = false;
    65. let latestFrameAt = null;
    66. let latestFrameAgeSeconds = null;
    67. try {
    68. const pid = Number(fs.readFileSync(PID_PATH, "utf8").trim());
    69. process.kill(pid, 0);
    70. running = true;
    71. } catch {
    72. running = false;
    73. }
    74. const latestFrames = walkFiles(SCREEN_ROOT, (filePath) => filePath.endsWith("-latest.jpg"));
    75. let newestMtime = 0;
    76. for (const framePath of latestFrames) {
    77. newestMtime = Math.max(newestMtime, statMtimeMs(framePath));
    78. }
    79. if (newestMtime) {
    80. latestFrameAt = new Date(newestMtime).toISOString();
    81. latestFrameAgeSeconds = Math.max(0, Math.round((Date.now() - newestMtime) / 1000));
    82. }
    83. return { running, latestFrameAt, latestFrameAgeSeconds };
    84. }
    85. function parseFrameTimestamp(filePath) {
    86. const fileName = path.basename(filePath);
    87. const minuteMatch = fileName.match(/frame-\d+-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z)\.jpg$/);
    88. if (minuteMatch) return parseChronicleTimestamp(minuteMatch[1]);
    89. const latestMatch = fileName.match(/^(.+)-display-\d+-latest\.jpg$/);
    90. if (latestMatch) return parseChronicleTimestamp(latestMatch[1]);
    91. return new Date(statMtimeMs(filePath)).toISOString();
    92. }
    93. function parseChronicleTimestamp(raw) {
    94. const normalized = raw
    95. .replace(/T(\d{2})-(\d{2})-(\d{2})/, "T$1:$2:$3")
    96. .replace(/\+00-00$/, "Z");
    97. const parsed = new Date(normalized);
    98. return Number.isNaN(parsed.getTime()) ? new Date().toISOString() : parsed.toISOString();
    99. }
    100. function parseSegmentKey(filePath) {
    101. const dirName = path.basename(path.dirname(filePath));
    102. if (dirName.includes("-display-")) return dirName;
    103. const fileName = path.basename(filePath);
    104. const latest = fileName.match(/^(.+-display-\d+)-latest\.jpg$/);
    105. if (latest) return latest[1];
    106. const sidecar = fileName.match(/^(.+-display-\d+)\.(ocr\.jsonl|capture|capture\.json)$/);
    107. if (sidecar) return sidecar[1];
    108. return "";
    109. }
    110. function parseDisplay(filePath) {
    111. const match = filePath.match(/display-(\d+)/);
    112. return match ? match[1] : "unknown";
    113. }
    114. function extractStrings(value, out = []) {
    115. if (typeof value === "string") {
    116. if (value.trim()) out.push(value.trim());
    117. return out;
    118. }
    119. if (Array.isArray(value)) {
    120. for (const item of value) extractStrings(item, out);
    121. return out;
    122. }
    123. if (value && typeof value === "object") {
    124. for (const item of Object.values(value)) extractStrings(item, out);
    125. }
    126. return out;
    127. }
    128. function loadOcrBySegment() {
    129. const sidecars = walkFiles(SCREEN_ROOT, (filePath) => filePath.endsWith(".ocr.jsonl"));
    130. const bySegment = new Map();
    131. for (const sidecarPath of sidecars) {
    132. const segmentKey = parseSegmentKey(sidecarPath);
    133. const snippets = [];
    134. try {
    135. const lines = fs.readFileSync(sidecarPath, "utf8").split(/\r?\n/).filter(Boolean);
    136. for (const line of lines.slice(-250)) {
    137. try {
    138. const strings = extractStrings(JSON.parse(line));
    139. snippets.push(...strings);
    140. } catch {
    141. snippets.push(line);
    142. }
    143. }
    144. } catch {
    145. continue;
    146. }
    147. const text = Array.from(new Set(snippets))
    148. .join(" ")
    149. .replace(/\s+/g, " ")
    150. .slice(0, 20000);
    151. bySegment.set(segmentKey, { sidecarPath, text });
    152. }
    153. return bySegment;
    154. }
    155. function loadMemories() {
    156. const files = walkFiles(MEMORY_ROOT, (filePath) => filePath.endsWith(".md"));
    157. return files
    158. .map((filePath) => {
    159. const fileName = path.basename(filePath);
    160. const stamp = fileName.match(/^(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})/);
    161. const timestamp = stamp ? parseChronicleTimestamp(`${stamp[1]}Z`) : new Date(statMtimeMs(filePath)).toISOString();
    162. let text = "";
    163. try {
    164. text = fs.readFileSync(filePath, "utf8");
    165. } catch {
    166. text = "";
    167. }
    168. const cwdMatch = text.match(/\bcwd=([^\n,)]+)/i) || text.match(/\bcwd:\s*([^\n]+)/i);
    169. const kind = fileName.includes("-6h-") ? "6h" : "10min";
    170. return {
    171. id: hashId(filePath),
    172. fileName,
    173. path: filePath,
    174. timestamp,
    175. timeMs: new Date(timestamp).getTime(),
    176. kind,
    177. cwd: cwdMatch ? cwdMatch[1].trim() : "",
    178. text,
    179. excerpt: text.replace(/^---[\s\S]*?---/, "").replace(/\s+/g, " ").trim().slice(0, 900)
    180. };
    181. })
    182. .sort((a, b) => a.timeMs - b.timeMs);
    183. }
    184. function nearestMemory(frameMs, memories) {
    185. let best = null;
    186. let bestDelta = Infinity;
    187. for (const memory of memories) {
    188. const delta = Math.abs(memory.timeMs - frameMs);
    189. const windowMs = memory.kind === "6h" ? 6 * 60 * 60 * 1000 : 16 * 60 * 1000;
    190. if (delta <= windowMs && delta < bestDelta) {
    191. best = memory;
    192. bestDelta = delta;
    193. }
    194. }
    195. return best;
    196. }
    197. function inferredProjects(text) {
    198. const matches = [];
    199. for (const [key, rule] of PROJECT_RULES) {
    200. if (rule.test(text)) matches.push(key);
    201. }
    202. return matches;
    203. }
    204. function thumbPathFor(item) {
    205. return path.join(THUMB_ROOT, `${item.id}.jpg`);
    206. }
    207. function ensureThumbnail(item) {
    208. ensureDir(THUMB_ROOT);
    209. const destination = thumbPathFor(item);
    210. const sourceMtime = statMtimeMs(item.path);
    211. const thumbMtime = statMtimeMs(destination);
    212. if (thumbMtime && thumbMtime >= sourceMtime) return destination;
    213. try {
    214. execFileSync("sips", ["-Z", "320", item.path, "--out", destination], {
    215. stdio: "ignore"
    216. });
    217. } catch {
    218. fs.copyFileSync(item.path, destination);
    219. }
    220. return destination;
    221. }
    222. function frameFiles() {
    223. const latest = walkFiles(SCREEN_ROOT, (filePath) => filePath.endsWith("-latest.jpg"));
    224. const historical = walkFiles(path.join(SCREEN_ROOT, "1min"), (filePath) => filePath.endsWith(".jpg"));
    225. return [...historical, ...latest];
    226. }
    227. function buildIndex({ withThumbs = false } = {}) {
    228. const status = chronicleStatus();
    229. const ocrBySegment = loadOcrBySegment();
    230. const memories = loadMemories();
    231. const frames = frameFiles();
    232. const items = frames
    233. .map((filePath) => {
    234. const timestamp = parseFrameTimestamp(filePath);
    235. const timeMs = new Date(timestamp).getTime();
    236. const segmentKey = parseSegmentKey(filePath);
    237. const ocr = ocrBySegment.get(segmentKey) || null;
    238. const memory = nearestMemory(timeMs, memories);
    239. const id = hashId(filePath);
    240. const fileName = path.basename(filePath);
    241. const haystack = [
    242. fileName,
    243. filePath,
    244. segmentKey,
    245. ocr?.text || "",
    246. memory?.fileName || "",
    247. memory?.cwd || "",
    248. memory?.text || ""
    249. ].join("\n");
    250. const item = {
    251. id,
    252. fileName,
    253. path: filePath,
    254. timestamp,
    255. timeMs,
    256. day: timestamp.slice(0, 10),
    257. display: parseDisplay(filePath),
    258. segmentKey,
    259. isLatest: fileName.endsWith("-latest.jpg"),
    260. thumbUrl: `/thumb/${id}.jpg`,
    261. frameUrl: `/frame/${id}.jpg`,
    262. ocrSidecarPath: ocr?.sidecarPath || "",
    263. ocrHint: ocr?.text ? ocr.text.slice(0, 900) : "",
    264. memory: memory
    265. ? {
    266. id: memory.id,
    267. fileName: memory.fileName,
    268. path: memory.path,
    269. timestamp: memory.timestamp,
    270. kind: memory.kind,
    271. cwd: memory.cwd,
    272. excerpt: memory.excerpt
    273. }
    274. : null,
    275. projects: inferredProjects(haystack),
    276. searchText: haystack.toLowerCase().slice(0, 50000)
    277. };
    278. if (withThumbs) ensureThumbnail(item);
    279. return item;
    280. })
    281. .sort((a, b) => b.timeMs - a.timeMs);
    282. const projectCounts = {};
    283. for (const item of items) {
    284. for (const project of item.projects) {
    285. projectCounts[project] = (projectCounts[project] || 0) + 1;
    286. }
    287. }
    288. return {
    289. generatedAt: new Date().toISOString(),
    290. screenRoot: SCREEN_ROOT,
    291. memoryRoot: MEMORY_ROOT,
    292. cacheRoot: CACHE_ROOT,
    293. status,
    294. counts: {
    295. frames: items.length,
    296. memories: memories.length,
    297. ocrSidecars: ocrBySegment.size
    298. },
    299. projectCounts,
    300. items
    301. };
    302. }
    303. function getIndex(force = false) {
    304. const now = Date.now();
    305. if (!force && indexCache && now - indexCacheAt < 15000) return indexCache;
    306. indexCache = buildIndex();
    307. indexCacheAt = now;
    308. return indexCache;
    309. }
    310. function itemById(id) {
    311. const index = getIndex();
    312. return index.items.find((item) => item.id === id);
    313. }
    314. function sendJson(res, payload, statusCode = 200) {
    315. const body = JSON.stringify(payload);
    316. res.writeHead(statusCode, {
    317. "content-type": "application/json; charset=utf-8",
    318. "cache-control": "no-store",
    319. "content-length": Buffer.byteLength(body)
    320. });
    321. res.end(body);
    322. }
    323. function sendFile(res, filePath, contentType = "application/octet-stream") {
    324. if (!fs.existsSync(filePath)) {
    325. res.writeHead(404);
    326. res.end("Not found");
    327. return;
    328. }
    329. res.writeHead(200, {
    330. "content-type": contentType,
    331. "cache-control": "no-store"
    332. });
    333. fs.createReadStream(filePath).pipe(res);
    334. }
    335. function contentTypeFor(filePath) {
    336. if (filePath.endsWith(".html")) return "text/html; charset=utf-8";
    337. if (filePath.endsWith(".css")) return "text/css; charset=utf-8";
    338. if (filePath.endsWith(".js")) return "text/javascript; charset=utf-8";
    339. if (filePath.endsWith(".jpg") || filePath.endsWith(".jpeg")) return "image/jpeg";
    340. if (filePath.endsWith(".png")) return "image/png";
    341. if (filePath.endsWith(".svg")) return "image/svg+xml";
    342. return "application/octet-stream";
    343. }
    344. function serveStatic(req, res, url) {
    345. const requested = url.pathname === "/" ? "/index.html" : url.pathname;
    346. const normalized = path.normal
    347. ...[truncated for intake]