/** * Scanner das sessões pi-code: percorre $PI_SESSIONS_DIR (glob recursivo * de arquivos .jsonl, todos os projetos) e alimenta o agregador. Tolerante: ignora * linhas malformadas, arquivos ilegíveis e não carrega arquivos em memória. */ import { createReadStream } from "node:fs"; import { readdir } from "node:fs/promises"; import { join } from "node:path"; import { createInterface } from "node:readline"; import { createAggregator, type SessionAggregate } from "./aggregate.ts"; const MAX_LINE_BYTES = 4 * 1024 * 1024; // linhas gigantes são truncadas pelo readline? não — protegemos abaixo async function* walkJsonl(root: string): AsyncGenerator { let entries; try { entries = await readdir(root, { withFileTypes: true }); } catch { return; // diretório inexistente/ilegível: fonte pi-code vazia, não erro fatal } for (const entry of entries) { const full = join(root, entry.name); if (entry.isDirectory()) { yield* walkJsonl(full); } else if (entry.isFile() && entry.name.endsWith(".jsonl")) { yield full; } } } export interface ScanResult { aggregate: SessionAggregate; } export async function scanSessions( sessionsDir: string, nowMs: number, timeZone: string, ): Promise { const { agg, addLine } = createAggregator(nowMs, timeZone); for await (const file of walkJsonl(sessionsDir)) { agg.files += 1; try { const rl = createInterface({ input: createReadStream(file, { encoding: "utf8" }), crlfDelay: Infinity, }); for await (const line of rl) { if (line.length > MAX_LINE_BYTES) continue; addLine(line); } } catch { agg.filesSkipped += 1; } } return { aggregate: agg }; }