import { createHash, randomUUID } from "node:crypto"; import { mkdir } from "node:fs/promises"; import { dirname, join } from "node:path"; export function nowMs(): number { return Date.now(); } export function newId(prefix: string): string { return `${prefix}-${randomUUID()}`; } export function stableStringify(value: unknown): string { if (value === null || typeof value !== "object") { return JSON.stringify(value); } if (Array.isArray(value)) { return `[${value.map(stableStringify).join(",")}]`; } const obj = value as Record; return `{${Object.keys(obj) .sort() .map((key) => `${JSON.stringify(key)}:${stableStringify(obj[key])}`) .join(",")}}`; } export function sha256Bytes(bytes: Buffer | string): string { return createHash("sha256").update(bytes).digest("hex"); } export async function ensureParent(path: string): Promise { await mkdir(dirname(path), { recursive: true }); } export function extensionDataDir(projectDir: string): string { return join(projectDir, ".pi", "extensions", "lore", "state"); } export function timeoutPromise( promise: Promise, timeoutMs: number, onTimeout: () => void, ): Promise { if (timeoutMs <= 0) { return promise; } let timer: NodeJS.Timeout | undefined; const timeout = new Promise((_, reject) => { timer = setTimeout(() => { try { onTimeout(); } finally { reject(new Error(`timed out after ${timeoutMs}ms`)); } }, timeoutMs); }); return Promise.race([promise, timeout]).finally(() => { if (timer) { clearTimeout(timer); } }); } export function textFromEntry(entry: { content?: unknown; text?: string }): string { if (typeof entry.text === "string") { return entry.text; } if (typeof entry.content === "string") { return entry.content; } if (Array.isArray(entry.content)) { return entry.content .map((part) => { if (typeof part === "string") return part; if (part && typeof part === "object" && "text" in part) { const text = (part as { text?: unknown }).text; return typeof text === "string" ? text : ""; } return ""; }) .filter(Boolean) .join("\n"); } return ""; } export function estimateTokens(text: string): number { return Math.ceil(text.length / 4); }