repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/session-rename/src/pi-ext-debug.ts
Raw// 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<string, unknown>;
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<Record<string, "number" | "boolean" | readonly string[]>>;
};
type FieldValue<T> = T extends "number"
? number
: T extends "boolean"
? boolean
: T extends readonly (infer V)[]
? V
: never;
type BoundDetails<S extends DebugSchema> = Partial<{
[K in keyof S["fields"]]: FieldValue<S["fields"][K]>;
}>;
type BoundDebug<S extends DebugSchema> = (
event: S["events"][number],
details?: BoundDetails<S>,
) => void;
type BoundSpan<S extends DebugSchema> = (
event: S["spans"][number],
details?: BoundDetails<S>,
) => (outcome?: "finish" | "error", details?: BoundDetails<S>) => void;
const extension = "session-rename";
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<string>;
function compileSchema(schema: DebugSchema):
| {
events: Set<string>;
spans: Set<string>;
fields: Map<string, FieldRule>;
}
| undefined {
try {
if (!Array.isArray(schema.events) || !Array.isArray(schema.spans))
return undefined;
const events = new Set<string>();
const spans = new Set<string>();
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<string, FieldRule>();
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<string, FieldRule>,
): 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<const S extends DebugSchema>(
schema: S,
): {
dbg: BoundDebug<S> | undefined;
span: BoundSpan<S> | 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<S> = (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<S> = (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_<SLUG>_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;