Files
pi-agent-integrated/pi-web/lib/session-file-references-core.ts

83 lines
2.5 KiB
TypeScript

import type { SessionEntry } from "./types";
const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
export function isValidSessionId(sessionId: string | null): sessionId is string {
return !!sessionId && SESSION_ID_RE.test(sessionId);
}
function safeDecode(value: string): string {
try {
return decodeURIComponent(value);
} catch {
return value;
}
}
function normalizeSlashes(value: string): string {
return value.replace(/\\/g, "/");
}
function isPathChar(ch: string): boolean {
return /[A-Za-z0-9._~+%@/\\:-]/.test(ch);
}
function hasReferenceBoundaryAfter(text: string, index: number): boolean {
if (index >= text.length) return true;
const ch = text[index];
if (ch === ":") return /\d/.test(text[index + 1] ?? "");
return !isPathChar(ch);
}
function containsExactPathReference(text: string, filePath: string): boolean {
const target = normalizeSlashes(filePath);
const targets = target.startsWith("/") ? [target, `file://${target}`] : [target];
const haystacks = new Set([normalizeSlashes(text), normalizeSlashes(safeDecode(text))]);
for (const haystack of haystacks) {
for (const t of targets) {
let index = haystack.indexOf(t);
while (index !== -1) {
const before = index === 0 ? "" : haystack[index - 1];
const afterIndex = index + t.length;
if ((index === 0 || !isPathChar(before)) && hasReferenceBoundaryAfter(haystack, afterIndex)) {
return true;
}
index = haystack.indexOf(t, index + 1);
}
}
}
return false;
}
function collectStrings(value: unknown, out: string[]): void {
if (typeof value === "string") {
out.push(value);
return;
}
if (!value || typeof value !== "object") return;
if (Array.isArray(value)) {
for (const item of value) collectStrings(item, out);
return;
}
for (const item of Object.values(value)) collectStrings(item, out);
}
export function isFilePathReferencedByEntries(filePath: string, entries: SessionEntry[]): boolean {
for (const entry of entries) {
const strings: string[] = [];
collectStrings(entry, strings);
if (strings.some((text) => containsExactPathReference(text, filePath))) return true;
}
return false;
}
export function isBashOutputPathReferencedByEntries(filePath: string, entries: SessionEntry[]): boolean {
return entries.some((entry) => (
entry.type === "message"
&& entry.message.role === "bashExecution"
&& entry.message.fullOutputPath === filePath
));
}