@earendil-works/pi-coding-agent
Version:
Coding agent CLI with read, bash, edit, write tools and session management
140 lines • 5.29 kB
JavaScript
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { getAgentDir, VERSION } from "../config.js";
import { isSyntheticPath } from "./source-info.js";
const MAX_CRASH_RECORDS = 5;
const MAX_AGE = 7 * 24 * 60 * 60 * 1000;
function crashLogPath(agentDir = getAgentDir()) {
return join(agentDir, "crashes.json");
}
export function readCrashLog(path = crashLogPath()) {
try {
const records = JSON.parse(readFileSync(path, "utf8"));
return Array.isArray(records)
? records.filter((record) => typeof record === "object" &&
record !== null &&
typeof record.timestamp === "string" &&
typeof record.message === "string")
: [];
}
catch {
return [];
}
}
function writeCrashLog(records, path) {
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, `${JSON.stringify(records, null, 2)}\n`);
}
function normalizeStackPath(value) {
return value.replace(/\\/g, "/").replace(/\/+$/u, "");
}
function stackContainsPath(stack, targetPath, includeDescendants) {
const target = normalizeStackPath(targetPath);
if (!target || isSyntheticPath(target))
return false;
const caseInsensitive = /^[a-z]:\//iu.test(target);
const haystack = caseInsensitive ? stack.toLowerCase() : stack;
const needle = caseInsensitive ? target.toLowerCase() : target;
if (includeDescendants)
return haystack.includes(`${needle}/`);
let index = haystack.indexOf(needle);
while (index !== -1) {
const next = haystack[index + needle.length];
if (next === undefined || next === ":" || next === ")" || /\s/u.test(next))
return true;
index = haystack.indexOf(needle, index + needle.length);
}
return false;
}
/** Find loaded extensions with source files in a stack trace. */
export function findExtensionStackMatches(stack, extensions) {
if (!stack)
return [];
const normalizedStack = stack
.split("\n")
.slice(1)
.filter((line) => /^\s+at\s/u.test(line))
.map((line) => {
try {
return decodeURI(line);
}
catch {
return line;
}
})
.join("\n")
.replace(/\\/g, "/");
const matches = [];
const seen = new Set();
for (const extension of extensions) {
const resolvedPath = normalizeStackPath(extension.resolvedPath);
const singleFilePackage = extension.sourceInfo.origin === "package" &&
!/^(?:npm:|git:|https?:\/\/|ssh:\/\/)/u.test(extension.sourceInfo.source) &&
/\.[cm]?[jt]s$/u.test(extension.sourceInfo.source);
const packageRoot = extension.sourceInfo.origin === "package" && !singleFilePackage && extension.sourceInfo.baseDir
? extension.sourceInfo.baseDir
: undefined;
const slashIndex = resolvedPath.lastIndexOf("/");
const directoryEntry = /\/index\.[cm]?[jt]s$/u.test(resolvedPath);
const matched = packageRoot
? stackContainsPath(normalizedStack, packageRoot, true)
: directoryEntry && slashIndex !== -1
? stackContainsPath(normalizedStack, resolvedPath.slice(0, slashIndex), true)
: stackContainsPath(normalizedStack, resolvedPath, false);
if (!matched)
continue;
const label = extension.sourceInfo.origin === "package" && extension.sourceInfo.source
? extension.sourceInfo.source
: extension.path;
if (!seen.has(label)) {
seen.add(label);
matches.push(label);
}
}
return matches;
}
/** Best-effort persistence for callers that are already crashing. */
export function recordCrash(crash, path = crashLogPath()) {
try {
const { error } = crash;
const record = {
timestamp: new Date().toISOString(),
version: VERSION,
kind: crash.kind,
message: error instanceof Error ? error.message || error.name : String(error),
stack: error instanceof Error && error.stack ? error.stack : null,
sessionFile: crash.sessionFile ?? null,
cwd: crash.cwd,
};
writeCrashLog([...readCrashLog(path), record].slice(-MAX_CRASH_RECORDS), path);
return record;
}
catch {
return undefined;
}
}
/** Return the newest recent crash, marking pending records as announced. */
export function takeUnnotifiedCrash(path = crashLogPath(), now = Date.now()) {
const records = readCrashLog(path);
const crash = [...records]
.reverse()
.find((record) => !record.notified && now - Date.parse(record.timestamp) <= MAX_AGE);
if (!crash)
return undefined;
try {
writeCrashLog(records.map((record) => (record.notified ? record : { ...record, notified: true })), path);
}
catch {
// Showing the notice again is harmless.
}
return crash;
}
export function clearCrashLog(path = crashLogPath()) {
try {
rmSync(path, { force: true });
}
catch {
// The records can be attached again if cleanup fails.
}
}
//# sourceMappingURL=crash-log.js.map