// Generated by scripts/codegen.mjs. DO NOT EDIT. // ============================================================================ // THIS FILE IS GENERATED. DO NOT EDIT IT DIRECTLY. // Instead, edit scripts/codegen/templates/pi-ext-debug.ts. // Then run: mise run //:codegen // ============================================================================ import { randomBytes } from "node:crypto"; import { closeSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, writeSync, } from "node:fs"; import { homedir } from "node:os"; import { isAbsolute, join } from "node:path"; export type DebugDetails = Record; export type Debug = (event: string, details?: DebugDetails) => void; export type SpanEnd = ( outcome?: "finish" | "error", details?: DebugDetails, ) => void; export type Span = (event: string, details?: DebugDetails) => SpanEnd; export type DebugSchema = { events: readonly string[]; spans: readonly string[]; fields: Readonly>; }; type FieldValue = T extends "number" ? number : T extends "boolean" ? boolean : T extends readonly (infer V)[] ? V : never; type BoundDetails = Partial<{ [K in keyof S["fields"]]: FieldValue; }>; type BoundDebug = ( event: S["events"][number], details?: BoundDetails, ) => void; type BoundSpan = ( event: S["spans"][number], details?: BoundDetails, ) => (outcome?: "finish" | "error", details?: BoundDetails) => void; const extension = "quota"; const maxFileBytes = 8 * 1024 * 1024; const maxMetadataFields = 24; const extensionDirectory = extension.toUpperCase().replaceAll("-", "_"); const classification = /^[a-z0-9][a-z0-9._-]{0,127}$/i; const eventName = /^[a-z0-9][a-z0-9._-]{0,127}$/i; const metadataKey = /^[a-z][a-z0-9]{0,63}$/i; const reservedFields = new Set([ "timestamp", "extension", "pid", "event", "spanId", "durationMs", "terminal", ]); const stringFields = new Set([ "api", "arch", "field", "kind", "mode", "modelId", "platform", "provider", "selector", "spanId", "status", "stopReason", "subtype", "type", ]); const numericFields = new Set([ "attempt", "budget", "ceiling", "count", "durationMs", "generation", "index", "length", "messageCount", "retries", "timeoutMs", ]); interface Writer { bytes: number; fd: number; } function userHome(): string { if (process.platform === "win32") return process.env.USERPROFILE ?? process.env.HOME ?? homedir(); return process.env.HOME ?? homedir(); } function globalDebugSelection(): boolean { try { const settings = JSON.parse( readFileSync( join( process.env.PI_CODING_AGENT_DIR || join(userHome(), ".pi", "agent"), "settings.json", ), "utf8", ), ) as { "pi-ext"?: { debug?: unknown } }; const debug = settings["pi-ext"]?.debug; return ( debug === true || debug === extension || (Array.isArray(debug) && debug.every((value) => typeof value === "string") && debug.includes(extension)) ); } catch { return false; } } function isEnabled(): boolean { switch (process.env[`PI_${extensionDirectory}_DEBUG`]) { case "1": return true; case "0": return false; default: return globalDebugSelection(); } } function debugRoot(): { base: string; parts: string[] } { if (process.platform === "darwin") return { base: join(userHome(), "Library", "Logs"), parts: ["pi-ext", extension], }; if (process.platform === "win32") return { base: process.env.LOCALAPPDATA || join(userHome(), "AppData", "Local"), parts: ["pi-ext", "debug", extension], }; return { base: process.env.XDG_STATE_HOME && isAbsolute(process.env.XDG_STATE_HOME) ? process.env.XDG_STATE_HOME : join(userHome(), ".local", "state"), parts: ["pi-ext", "debug", extension], }; } function ensurePrivateDirectory(): string { const { base, parts } = debugRoot(); mkdirSync(base, { recursive: true, mode: 0o700 }); let directory = base; for (const part of parts) { directory = join(directory, part); // Existing directories are the common case; probing first avoids a thrown EEXIST per level. let stat = lstatSync(directory, { throwIfNoEntry: false }); if (!stat) { try { mkdirSync(directory, { mode: 0o700 }); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; } stat = lstatSync(directory); } if ( !stat.isDirectory() || stat.isSymbolicLink() || (process.platform !== "win32" && (stat.mode & 0o077) !== 0) ) throw new Error("debug directory is not private"); } return directory; } function createWriter(): Writer | undefined { try { const directory = ensurePrivateDirectory(); for (let attempt = 0; attempt < 3; attempt += 1) { const path = join( directory, `${new Date().toISOString().replace(/[.:]/g, "-")}-${process.pid}-${randomBytes(6).toString("hex")}.jsonl`, ); try { const fd = openSync(path, "wx", 0o600); const stat = fstatSync(fd); if ( !stat.isFile() || (process.platform !== "win32" && (stat.mode & 0o077) !== 0) ) { closeSync(fd); throw new Error("debug file is not private"); } return { bytes: 0, fd }; } catch { // Random suffixes make collisions improbable; debug failures stay silent. } } } catch { // Debug logging must never affect extension work or Pi output. } return undefined; } function safeDetails(details: DebugDetails): DebugDetails { const safe: DebugDetails = {}; for (const [key, value] of Object.entries(details)) { if (Object.keys(safe).length >= maxMetadataFields) break; if (!metadataKey.test(key)) continue; if (typeof value === "boolean") { safe[key] = value; continue; } if ( typeof value === "number" && Number.isFinite(value) && Math.abs(value) <= Number.MAX_SAFE_INTEGER && numericFields.has(key) ) { safe[key] = value; continue; } if ( typeof value === "string" && stringFields.has(key) && classification.test(value) ) safe[key] = value; } return safe; } type FieldRule = "number" | "boolean" | Set; function compileSchema(schema: DebugSchema): | { events: Set; spans: Set; fields: Map; } | undefined { try { if (!Array.isArray(schema.events) || !Array.isArray(schema.spans)) return undefined; const events = new Set(); const spans = new Set(); for (const name of schema.events) { if ( typeof name !== "string" || !eventName.test(name) || name.startsWith("debug.") ) return undefined; events.add(name); } for (const name of schema.spans) { if ( typeof name !== "string" || !eventName.test(`${name}.finish`) || name.startsWith("debug.") ) return undefined; spans.add(name); } const fields = new Map(); if (!schema.fields || typeof schema.fields !== "object") return undefined; for (const [key, rule] of Object.entries(schema.fields)) { if (!metadataKey.test(key) || reservedFields.has(key)) return undefined; if (rule === "number" || rule === "boolean") fields.set(key, rule); else if ( Array.isArray(rule) && rule.length > 0 && rule.every( (value) => typeof value === "string" && classification.test(value), ) ) fields.set(key, new Set(rule)); else return undefined; } return { events, spans, fields }; } catch { return undefined; } } function boundDetails( details: DebugDetails | undefined, fields: Map, ): DebugDetails { const safe: DebugDetails = {}; if (!details || typeof details !== "object") return safe; for (const [key, rule] of fields) { if (Object.keys(safe).length >= maxMetadataFields) break; const descriptor = Object.getOwnPropertyDescriptor(details, key); if (!descriptor || !("value" in descriptor)) continue; const value = descriptor.value; if (rule === "boolean" && typeof value === "boolean") safe[key] = value; else if ( rule === "number" && typeof value === "number" && Number.isFinite(value) && Math.abs(value) <= Number.MAX_SAFE_INTEGER ) safe[key] = value; else if ( rule instanceof Set && typeof value === "string" && rule.has(value) ) safe[key] = value; } return safe; } function write(writer: Writer, bytes: Buffer): void { let offset = 0; while (offset < bytes.length) { const written = writeSync(writer.fd, bytes, offset, bytes.length - offset); if (written <= 0) throw new Error("debug write made no progress"); offset += written; } writer.bytes += bytes.length; } function record( event: string, details: DebugDetails = {}, safeDetailsOverride?: DebugDetails, ): void { if (stopped) return; writer ??= createWriter(); if (!writer) { stopped = true; return; } try { const timestamp = new Date().toISOString(); const safe = safeDetailsOverride ?? safeDetails(details); const line = Buffer.from( `${JSON.stringify({ ...safe, timestamp, extension, pid: process.pid, event: eventName.test(event) ? event : "debug.invalid_event", })}\n`, "utf8", ); if (writer.bytes + line.length + terminalBytes > maxFileBytes) { if (writer.bytes + terminalBytes <= maxFileBytes) write(writer, terminalRecord(timestamp)); stop(); return; } write(writer, line); } catch { stop(); } } function terminalRecord(timestamp: string): Buffer { return Buffer.from( `${JSON.stringify({ timestamp, extension, pid: process.pid, event: "debug.limit", terminal: true, })}\n`, "utf8", ); } // ISO timestamps have a fixed width, so the terminal record size is constant per process. const terminalBytes = terminalRecord(new Date(0).toISOString()).length; function closeWriter(): void { if (writer) { try { closeSync(writer.fd); } catch { // Debug failures never affect extension work. } writer = undefined; } } function stop(): void { stopped = true; closeWriter(); } export const closeDebug = closeWriter; /** Bind extension-owned event and metadata declarations to the local writer. */ export function defineDebug( schema: S, ): { dbg: BoundDebug | undefined; span: BoundSpan | undefined; closeDebug: typeof closeDebug; } { if (!dbg) return { dbg: undefined, span: undefined, closeDebug }; const compiled = compileSchema(schema); if (!compiled) return { dbg: undefined, span: undefined, closeDebug }; const boundDbg: BoundDebug = (event, details) => { if (!compiled.events.has(event)) return; try { record(event, {}, boundDetails(details, compiled.fields)); } catch { // Bad metadata must not affect extension work or leak into diagnostics. } }; const boundSpan: BoundSpan = (event, details) => { if (!compiled.spans.has(event)) return () => {}; const spanId = `${process.pid}-${++nextSpanId}`; const started = performance.now(); try { record( `${event}.start`, {}, { ...boundDetails(details, compiled.fields), spanId, }, ); } catch { // Bad metadata must not affect extension work or leak into diagnostics. } let completed = false; return (outcome = "finish", finishDetails) => { if (completed) return; completed = true; if (outcome !== "finish" && outcome !== "error") return; try { record( `${event}.${outcome}`, {}, { ...boundDetails(finishDetails, compiled.fields), spanId, durationMs: Math.max(0, performance.now() - started), }, ); } catch { // Bad metadata must not affect extension work or leak into diagnostics. } }; }; return { dbg: boundDbg, span: boundSpan, closeDebug }; } let writer: Writer | undefined; let stopped = false; let nextSpanId = 0; /** Metadata-only extension tracing, selected through user settings or PI__DEBUG. */ export const dbg: Debug | undefined = isEnabled() ? record : undefined; /** Monotonic operation spans. Call through optional chaining to avoid disabled work. */ export const span: Span | undefined = dbg ? (event, details = {}) => { const spanId = `${process.pid}-${++nextSpanId}`; const started = performance.now(); record(`${event}.start`, { ...details, spanId }); let completed = false; return (outcome = "finish", finishDetails = {}) => { if (completed) return; completed = true; record(`${event}.${outcome}`, { ...finishDetails, spanId, durationMs: Math.max(0, performance.now() - started), }); }; } : undefined;