repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
pi/agent/skills/pi-sessions/scripts/inspect.mjs
Rawimport { createReadStream } from "node:fs";
import { lstat, readFile, readdir } from "node:fs/promises";
import { homedir } from "node:os";
import { basename, join, resolve } from "node:path";
import { createInterface } from "node:readline";
import { parseArgs } from "node:util";
const help = `Read-only Pi session inspection (JSON output).
inspect.mjs list [literal-query] [--cwd PATH] [--since YYYY-MM-DD] [--until YYYY-MM-DD]
inspect.mjs show <id-prefix|exact-name|discovered-file> [--leaf ID] [--view branch|context]
--session-dir PATH Override session storage; never scan HOME
--offset N --limit N Page results (default 0/20 for list, 0/50 for show; max 100)
--text Include bounded user/assistant/summary excerpts
--content Include exact persisted entry payloads, including tool calls and results
--help Show usage
`;
const textLimit = 500;
const clipped = (value) => (typeof value === "string" ? value.slice(0, textLimit) : undefined);
const fail = (file, line, reason) => {
throw new Error(`${file}${line ? `: line ${line}` : ""}: ${reason}`);
};
async function info(path) {
try {
return await lstat(path);
} catch (error) {
if (error.code === "ENOENT") return null;
throw new Error(`${path}: ${error.code ?? "stat failed"}`);
}
}
async function settings(path) {
const stat = await info(path);
if (!stat) return {};
if (!stat.isFile()) fail(path, null, "settings must be a regular file");
let data;
try {
data = JSON.parse(await readFile(path, "utf8"));
} catch {
fail(path, null, "cannot parse settings JSON");
}
if (!data || typeof data !== "object" || Array.isArray(data)) fail(path, null, "settings must be an object");
if (data.sessionDir !== undefined && (typeof data.sessionDir !== "string" || !data.sessionDir.trim()))
fail(path, null, "sessionDir must be a nonempty string");
return { sessionDir: data.sessionDir };
}
async function filesUnder(root) {
const stat = await info(root);
if (!stat) return [];
if (!stat.isDirectory()) fail(root, null, "session root must be a directory, not a symlink");
const files = [];
for (const item of await readdir(root, { withFileTypes: true })) {
const path = join(root, item.name);
if (item.isDirectory()) files.push(...(await filesUnder(path)));
else if (item.isFile() && item.name.endsWith(".jsonl")) files.push(path);
}
return files.sort();
}
async function discover(options, { cwd, home, env }) {
const expand = (path) => resolve(cwd, path === "~" ? home : path.replace(/^~[/\\]/, `${home}/`));
const agent = expand(env.PI_CODING_AGENT_DIR || join(home, ".pi/agent"));
const global = await settings(join(agent, "settings.json"));
const project = await settings(join(cwd, ".pi/settings.json"));
const configured = [
["--session-dir", options["session-dir"]],
["PI_CODING_AGENT_SESSION_DIR", env.PI_CODING_AGENT_SESSION_DIR],
["project settings", project.sessionDir],
["global settings", global.sessionDir],
].find(([, path]) => path);
const roots = configured
? [configured]
: [
["state fallback", join(home, ".local/state/pi/sessions")],
["agent fallback", join(agent, "sessions")],
["project fallback", join(cwd, ".pi/sessions")],
];
for (const [source, path] of roots) {
const root = expand(path);
const files = await filesUnder(root);
if (configured || files.length) return { source, root, files };
}
return { source: "no sessions found", root: null, files: [] };
}
function projectMessage(message, withText) {
const row = { role: clipped(message?.role) };
if (!row.role) throw new Error("message role is missing");
const blocks = Array.isArray(message.content) ? message.content : [];
const calls = blocks.filter((b) => b?.type === "toolCall");
row.callCount = calls.length;
if (calls.length)
row.calls = calls.slice(0, 20).map((call) => ({ id: clipped(call.id), name: clipped(call.name) }));
if (message.role === "assistant") {
row.provider = clipped(message.provider);
row.model = clipped(message.model);
row.stopReason = clipped(message.stopReason);
}
if (message.role === "toolResult") {
row.tool = clipped(message.toolName);
row.toolCallId = clipped(message.toolCallId);
row.isError = message.isError === true;
}
if (message.role === "bashExecution") {
row.exitCode = Number.isInteger(message.exitCode) ? message.exitCode : null;
row.cancelled = message.cancelled === true;
}
if (withText && ["user", "assistant"].includes(message.role)) {
const text =
typeof message.content === "string"
? message.content
: blocks
.filter((b) => b?.type === "text" && typeof b.text === "string")
.map((b) => b.text)
.join("\n");
row.text = clipped(text);
row.textTruncated = text.length > textLimit;
}
return row;
}
async function readSession(file, withText, scope, withContent) {
const stat = await info(file);
if (!stat?.isFile()) fail(file, null, "session must be a regular file");
if (!stat.size) fail(file, 1, "empty session");
// Freeze the read boundary: live appends must not extend this inspection.
const stream = createReadStream(file, { encoding: "utf8", end: stat.size - 1 });
const lines = createInterface({ input: stream, crlfDelay: Infinity });
const entries = [];
const byId = new Map();
let header;
let name = null;
let line = 0;
try {
for await (const text of lines) {
line++;
if (!text.trim()) continue;
let entry;
try {
entry = JSON.parse(text);
} catch {
fail(file, line, "malformed JSONL (possibly an unfinished live write)");
}
if (!header) {
if (
entry?.type !== "session" ||
typeof entry.id !== "string" ||
!entry.id ||
typeof entry.cwd !== "string" ||
typeof entry.timestamp !== "string" ||
!Number.isFinite(Date.parse(entry.timestamp))
)
fail(file, line, "invalid session header");
if (entry.version !== 3) fail(file, line, "unsupported session version; expected 3");
header = { file, id: entry.id, cwd: entry.cwd, started: new Date(entry.timestamp).toISOString() };
if (scope && !scope(header)) return null;
continue;
}
if (
!entry ||
typeof entry.id !== "string" ||
!entry.id ||
typeof entry.type !== "string" ||
!(entry.parentId === null || typeof entry.parentId === "string")
)
fail(file, line, "invalid entry shape");
if (byId.has(entry.id)) fail(file, line, "duplicate entry ID");
if (entry.parentId !== null && !byId.has(entry.parentId)) fail(file, line, "missing or forward parent");
const row = {
id: entry.id,
parentId: entry.parentId,
type: entry.type,
line,
time: clipped(entry.timestamp),
};
if (withContent) {
const { id, parentId, type, timestamp, ...payload } = entry;
row.payload = payload;
}
if (entry.type === "message") {
try {
Object.assign(row, projectMessage(entry.message, withText));
} catch {
fail(file, line, "invalid message");
}
}
if (entry.type === "session_info") {
if (typeof entry.name === "string") name = entry.name;
row.name = clipped(entry.name);
}
if (entry.type === "model_change") {
row.provider = clipped(entry.provider);
row.model = clipped(entry.modelId);
}
if (entry.type === "thinking_level_change") row.thinkingLevel = clipped(entry.thinkingLevel);
if (entry.type === "custom" || entry.type === "custom_message") row.customType = clipped(entry.customType);
if (entry.type === "compaction") {
row.tokensBefore = Number.isFinite(entry.tokensBefore) ? entry.tokensBefore : null;
if (entry.firstKeptEntryId !== undefined && typeof entry.firstKeptEntryId !== "string")
fail(file, line, "firstKeptEntryId must be a string");
row.firstKeptEntryId = entry.firstKeptEntryId;
if (entry.retainedTail !== undefined) {
if (!Array.isArray(entry.retainedTail)) fail(file, line, "retainedTail must be an array");
try {
row.retainedTail = entry.retainedTail.map((message, i) => ({
id: `${entry.id}:tail:${i}`,
type: "message",
line,
retainedIndex: i,
...projectMessage(message, withText),
...(withContent ? { payload: { message } } : {}),
}));
} catch {
fail(file, line, "invalid retained-tail message");
}
}
}
if (withText && ["compaction", "branch_summary"].includes(entry.type)) {
row.text = clipped(entry.summary);
row.textTruncated = typeof entry.summary === "string" && entry.summary.length > textLimit;
}
entries.push(row);
byId.set(row.id, row);
}
} finally {
lines.close();
stream.destroy();
}
if (!header) fail(file, 1, "missing session header");
return { ...header, name, updated: entries.at(-1)?.time ?? header.started, entries, byId };
}
function branch(session, leafId = session.entries.at(-1)?.id) {
const result = [];
if (leafId && !session.byId.has(leafId)) throw new Error("Unknown leaf ID");
let current = session.byId.get(leafId);
while (current) {
result.push(current);
current = session.byId.get(current.parentId);
}
return result.reverse();
}
function context(path, file) {
const index = path.findLastIndex((e) => e.type === "compaction");
if (index < 0) return path;
const compaction = path[index];
let kept = compaction.retainedTail;
if (kept === undefined) {
const start = path.findIndex((e) => e.id === compaction.firstKeptEntryId);
if (start < 0 || start >= index)
fail(file, compaction.line, "compaction has no valid firstKeptEntryId or retainedTail");
kept = path.slice(start, index);
}
return [compaction, ...kept, ...path.slice(index + 1)];
}
function counts(rows) {
const count = (predicate) => rows.filter(predicate).length;
return {
users: count((r) => r.role === "user"),
assistants: count((r) => r.role === "assistant"),
toolCalls: rows.reduce((n, r) => n + (r.callCount ?? 0), 0),
toolErrors: count((r) => r.role === "toolResult" && r.isError),
assistantErrors: count((r) => r.stopReason === "error"),
aborts: count((r) => r.stopReason === "aborted" || r.cancelled),
bashErrors: count((r) => r.role === "bashExecution" && r.exitCode !== null && r.exitCode !== 0),
compactions: count((r) => r.type === "compaction"),
};
}
function summary(session) {
const path = branch(session);
return {
file: session.file,
id: session.id,
name: clipped(session.name),
cwd: session.cwd,
started: session.started,
updated: session.updated,
leafId: path.at(-1)?.id ?? null,
counts: counts(path),
};
}
function pageNumber(value, fallback, name, max = Number.MAX_SAFE_INTEGER) {
if (value === undefined) return fallback;
if (
!/^\d+$/.test(value) ||
!Number.isSafeInteger(Number(value)) ||
Number(value) > max ||
(name === "limit" && Number(value) === 0)
)
throw new Error(`Invalid ${name}`);
return Number(value);
}
function date(value) {
if (value === undefined) return undefined;
if (
!/^\d{4}-\d{2}-\d{2}$/.test(value) ||
!Number.isFinite(Date.parse(value)) ||
new Date(value).toISOString().slice(0, 10) !== value
)
throw new Error("Invalid date; use YYYY-MM-DD");
return value;
}
export async function inspect(args, { cwd = process.cwd(), home = homedir(), env = process.env } = {}) {
const { values, positionals } = parseArgs({
args,
allowPositionals: true,
options: {
"session-dir": { type: "string" },
cwd: { type: "string" },
since: { type: "string" },
until: { type: "string" },
offset: { type: "string" },
limit: { type: "string" },
leaf: { type: "string" },
view: { type: "string" },
text: { type: "boolean" },
content: { type: "boolean" },
help: { type: "boolean" },
},
});
if (values.help) return { help };
const [command = "list", query] = positionals;
if (!["list", "show"].includes(command) || positionals.length > 2 || (command === "show" && !query))
throw new Error("Use list [query] or show <selector>; see --help");
const offset = pageNumber(values.offset, 0, "offset");
const limit = pageNumber(values.limit, command === "list" ? 20 : 50, "limit", 100);
const since = date(values.since);
const until = date(values.until);
if (since && until && since > until) throw new Error("Date range is reversed");
const view = values.view ?? "branch";
if (!["branch", "context"].includes(view)) throw new Error("View must be branch or context");
if (values["session-dir"] === "" || values.leaf === "")
throw new Error("Session directory and leaf must not be empty");
const store = await discover(values, { cwd, home, env });
const direct =
command === "show"
? store.files.filter((file) => file === resolve(cwd, query) || basename(file) === query)
: [];
const cwdFilter = values.cwd ? resolve(cwd, values.cwd) : undefined;
// Only a validated header outside explicit scope may bypass body validation.
const scope =
command === "list" && (cwdFilter || since || until)
? (header) =>
(!cwdFilter || header.cwd === cwdFilter) &&
(!since || header.started.slice(0, 10) >= since) &&
(!until || header.started.slice(0, 10) <= until)
: undefined;
const sessions = [];
const errors = [];
let skipped = 0;
for (const file of direct.length ? direct : store.files) {
try {
const session = await readSession(file, values.text, scope, values.content);
if (session) sessions.push(session);
else skipped++;
} catch (error) {
if (direct.length === 1) throw error;
errors.push({ file, error: error.message });
}
}
const status = {
root: store.root,
source: store.source,
scanned: direct.length || store.files.length,
parsed: sessions.length,
skipped,
complete: errors.length === 0,
errorCount: errors.length,
errors: errors.slice(0, limit),
};
if (command === "list") {
const queryLower = query?.toLowerCase();
const selected = sessions
.filter(
(s) =>
(!queryLower || [s.name, s.id, s.cwd].some((v) => v?.toLowerCase().includes(queryLower))) &&
(!cwdFilter || s.cwd === cwdFilter) &&
(!since || s.started.slice(0, 10) >= since) &&
(!until || s.started.slice(0, 10) <= until),
)
.sort((a, b) => b.updated.localeCompare(a.updated) || a.file.localeCompare(b.file));
return {
...status,
total: selected.length,
offset,
limit,
sessions: selected.slice(offset, offset + limit).map((s) => ({
...summary(s),
...(values.text ? { firstPrompt: branch(s).find((e) => e.role === "user")?.text } : {}),
})),
};
}
const exact = sessions.filter((s) => s.id === query);
const selected = direct.length
? sessions
: exact.length
? exact
: sessions.filter((s) => s.name === query || s.id.startsWith(query));
if (selected.length > 1)
throw new Error(
`Ambiguous session selector (${selected.length} matches); list then choose an exact ID or file`,
);
if (!selected.length)
throw new Error(`Session not found${errors.length ? `; ${errors.length} files could not be read` : ""}`);
const session = selected[0];
const path = branch(session, values.leaf);
const retained = context(path, session.file);
const rows = view === "branch" ? path : retained;
const parents = new Set(session.entries.map((e) => e.parentId));
const heads = session.entries.filter((e) => !parents.has(e.id)).map((e) => e.id);
return {
...status,
...summary(session),
counts: counts(path),
view,
leafId: path.at(-1)?.id ?? null,
leafSource: values.leaf ? "explicit" : "last-persisted-entry",
branch: { entries: path.length, counts: counts(path) },
context: { entries: retained.length, counts: counts(retained) },
heads: { total: heads.length, ids: heads.slice(0, limit) },
total: rows.length,
offset,
limit,
...(values.text || values.content ? { historicalContentIsUntrusted: true } : {}),
timeline: rows.slice(offset, offset + limit).map(({ retainedTail, firstKeptEntryId, ...row }) => ({
...row,
...(retainedTail ? { retainedMessages: retainedTail.length } : {}),
...(firstKeptEntryId ? { firstKeptEntryId } : {}),
})),
};
}
if (import.meta.main) {
try {
const result = await inspect(process.argv.slice(2));
process.stdout.write(result.help ?? `${JSON.stringify(result, null, 2)}\n`);
if (result.complete === false) process.exitCode = 1;
} catch (error) {
process.stderr.write(`${JSON.stringify({ error: error.message })}\n`);
process.exitCode = 1;
}
}