Files
oh-my-pi/packages/stats/src/parser.ts
T
can1357 d9a3291641 feat(deps): replaced external JSON/JSONL and text utilities with Bun built-in APIs
- Replaced external JSON/JSONL parsing libraries with Bun's built-in JSON5 and JSONL APIs across all packages.
- Removed dependencies: json5, ndjson, get-east-asian-width, json-stringify-safe, split2, through2, and @types/ndjson.
- Replaced custom text width and ANSI wrapping implementations with Bun.stringWidth() and Bun.wrapAnsi() APIs.
- Updated Bun type definitions from ^1.2.17 to ^1.2.18 and bun-types from ^1.3.5 to ^1.3.7.
- Refactored JSONL parsing throughout codebase to use Bun.JSONL.parse() and Bun.JSONL.parseChunk() with improved buffer management and error handling.
- Updated test assertions to use Bun.stringWidth() for dynamic width calculations instead of hardcoded values.
2026-01-27 19:18:58 +01:00

155 lines
4.1 KiB
TypeScript

import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
import type { MessageStats, SessionEntry, SessionMessageEntry } from "./types";
const SESSIONS_DIR = path.join(os.homedir(), ".omp", "agent", "sessions");
/**
* Extract folder name from session filename.
* Session files are named like: --work--pi--/timestamp_uuid.jsonl
* The folder part uses -- as path separator.
*/
function extractFolderFromPath(sessionPath: string): string {
const dir = path.basename(sessionPath.replace(/\/[^/]+\.jsonl$/, ""));
// Convert --work--pi-- to /work/pi
return dir.replace(/^--/, "/").replace(/--/g, "/");
}
/**
* Check if an entry is an assistant message.
*/
function isAssistantMessage(entry: SessionEntry): entry is SessionMessageEntry {
if (entry.type !== "message") return false;
const msgEntry = entry as SessionMessageEntry;
return msgEntry.message?.role === "assistant";
}
/**
* Extract stats from an assistant message entry.
*/
function extractStats(sessionFile: string, folder: string, entry: SessionMessageEntry): MessageStats | null {
const msg = entry.message as AssistantMessage;
if (!msg || msg.role !== "assistant") return null;
return {
sessionFile,
entryId: entry.id,
folder,
model: msg.model,
provider: msg.provider,
api: msg.api,
timestamp: msg.timestamp,
duration: msg.duration ?? null,
ttft: msg.ttft ?? null,
stopReason: msg.stopReason,
errorMessage: msg.errorMessage ?? null,
usage: msg.usage,
};
}
/**
* Parse a session file and extract all assistant message stats.
* Uses incremental reading with offset tracking.
*/
export async function parseSessionFile(
sessionPath: string,
fromOffset = 0,
): Promise<{ stats: MessageStats[]; newOffset: number }> {
const file = Bun.file(sessionPath);
const exists = await file.exists();
if (!exists) {
return { stats: [], newOffset: fromOffset };
}
const text = await file.text();
const entries = Bun.JSONL.parse(text) as SessionEntry[];
const lines = text.split("\n");
const folder = extractFolderFromPath(sessionPath);
const stats: MessageStats[] = [];
let currentOffset = 0;
let entryIndex = 0;
for (const line of lines) {
const lineLength = line.length + 1; // +1 for newline
if (line.trim()) {
const entry = entries[entryIndex];
if (currentOffset >= fromOffset && entry && isAssistantMessage(entry)) {
const msgStats = extractStats(sessionPath, folder, entry);
if (msgStats) {
stats.push(msgStats);
}
}
entryIndex += 1;
}
currentOffset += lineLength;
}
return { stats, newOffset: currentOffset };
}
/**
* List all session directories (folders).
*/
export async function listSessionFolders(): Promise<string[]> {
try {
const entries = await fs.readdir(SESSIONS_DIR, { withFileTypes: true });
return entries.filter(e => e.isDirectory()).map(e => path.join(SESSIONS_DIR, e.name));
} catch {
return [];
}
}
/**
* List all session files in a folder.
*/
export async function listSessionFiles(folderPath: string): Promise<string[]> {
try {
const entries = await fs.readdir(folderPath, { withFileTypes: true });
return entries.filter(e => e.isFile() && e.name.endsWith(".jsonl")).map(e => path.join(folderPath, e.name));
} catch {
return [];
}
}
/**
* List all session files across all folders.
*/
export async function listAllSessionFiles(): Promise<string[]> {
const folders = await listSessionFolders();
const allFiles: string[] = [];
for (const folder of folders) {
const files = await listSessionFiles(folder);
allFiles.push(...files);
}
return allFiles;
}
/**
* Get session directory path.
*/
export function getSessionsDir(): string {
return SESSIONS_DIR;
}
/**
* Find a specific entry in a session file.
*/
export async function getSessionEntry(sessionPath: string, entryId: string): Promise<SessionEntry | null> {
const file = Bun.file(sessionPath);
if (!(await file.exists())) return null;
const text = await file.text();
const entries = Bun.JSONL.parse(text) as SessionEntry[];
for (const entry of entries) {
if ("id" in entry && entry.id === entryId) {
return entry;
}
}
return null;
}