mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
* Run Pi through its RPC interface Replace the embedded Pi runtime packages with a process-backed runtime that starts pi --mode rpc, keeps the runtime port thin, and leaves provider behavior in the Paseo session and mapper modules. Add focused adapter and mapper tests plus real Pi E2E coverage for the installed binary path. * Keep Pi provider code in one module * Add Pi session imports * Enable Paseo MCP tools in Pi agents Route injected MCP servers through Pi's MCP adapter and make structured MCP results visible to model-only clients. Normalize Pi MCP proxy calls so Paseo timelines show the underlying tool names. * Fix Pi import symlink tests on Windows Create directory links explicitly in symlink cwd fixtures so Windows server tests exercise the intended path-equivalence behavior. * Use native realpath for import cwd matching Resolve import cwd matchers with native realpath so Windows symlink and junction sessions compare against the same path shape used by persisted provider data. * Fallback realpath for Windows import matching Use plain realpath when native realpath cannot resolve a cwd so symlink-equivalent persisted sessions still match on Windows runners. * Add daemon system prompt support for Pi * Fix Windows realpath import matching * Remove Pi RPC migration plan doc
134 lines
4.5 KiB
TypeScript
134 lines
4.5 KiB
TypeScript
import os from "node:os";
|
|
import { realpathSync } from "node:fs";
|
|
import nodePath from "node:path";
|
|
|
|
/**
|
|
* Expand tilde in path to home directory
|
|
*/
|
|
export function expandTilde(path: string): string {
|
|
if (path.startsWith("~/")) {
|
|
const homeDir = process.env.HOME || os.homedir();
|
|
return path.replace("~", homeDir);
|
|
}
|
|
if (path === "~") {
|
|
return process.env.HOME || os.homedir();
|
|
}
|
|
return path;
|
|
}
|
|
|
|
/**
|
|
* Compare two path strings as filesystem-equivalent for cwd filtering.
|
|
*
|
|
* This is a string-only comparison: it normalizes separators and dot segments,
|
|
* ignores trailing separators, strips Windows namespace prefixes, and
|
|
* case-folds only when comparing as Windows. It does not resolve symlinks or
|
|
* check whether either path exists.
|
|
*/
|
|
export function areEquivalentPaths(left: string, right: string): boolean {
|
|
const compareAsWindows = shouldCompareAsWindows(left, right);
|
|
return (
|
|
normalizePathForComparison(left, compareAsWindows) ===
|
|
normalizePathForComparison(right, compareAsWindows)
|
|
);
|
|
}
|
|
|
|
export function createPathEquivalenceMatcher(target: string): (candidate: string) => boolean {
|
|
const targetLooksWindows = looksLikeDefiniteWindowsPath(target);
|
|
const compareAsWindows = targetLooksWindows;
|
|
const normalizedTarget = normalizePathForComparison(target, compareAsWindows);
|
|
|
|
return (candidate) => {
|
|
const candidateCompareAsWindows = compareAsWindows || looksLikeDefiniteWindowsPath(candidate);
|
|
const comparableTarget =
|
|
candidateCompareAsWindows === compareAsWindows
|
|
? normalizedTarget
|
|
: normalizePathForComparison(target, candidateCompareAsWindows);
|
|
return normalizePathForComparison(candidate, candidateCompareAsWindows) === comparableTarget;
|
|
};
|
|
}
|
|
|
|
export function createRealpathAwarePathMatcher(target: string): (candidate: string) => boolean {
|
|
const targetMatchers = collectPathVariants(target).map((variant) =>
|
|
createPathEquivalenceMatcher(variant),
|
|
);
|
|
|
|
return (candidate) => {
|
|
const candidateVariants = collectPathVariants(candidate);
|
|
return candidateVariants.some((variant) => targetMatchers.some((matches) => matches(variant)));
|
|
};
|
|
}
|
|
|
|
function collectPathVariants(value: string): string[] {
|
|
const variants = new Set<string>([value]);
|
|
for (const realpath of resolveRealpathVariants(value)) {
|
|
variants.add(realpath);
|
|
}
|
|
return Array.from(variants);
|
|
}
|
|
|
|
function resolveRealpathVariants(value: string): string[] {
|
|
const variants: string[] = [];
|
|
try {
|
|
variants.push(realpathSync.native(value));
|
|
} catch {
|
|
// Path may not exist, or the platform may reject this link flavor.
|
|
}
|
|
try {
|
|
variants.push(realpathSync(value));
|
|
} catch {
|
|
// Keep string-only comparison as the fallback.
|
|
}
|
|
return variants;
|
|
}
|
|
|
|
function shouldCompareAsWindows(left: string, right: string): boolean {
|
|
return looksLikeDefiniteWindowsPath(left) || looksLikeDefiniteWindowsPath(right);
|
|
}
|
|
|
|
function looksLikeDefiniteWindowsPath(value: string): boolean {
|
|
return (
|
|
/^[a-zA-Z]:[\\/]/u.test(value) ||
|
|
/^[/\\]{2}\?[/\\]/u.test(value) ||
|
|
/^\\{2}[^/\\]+[/\\][^/\\]+/u.test(value)
|
|
);
|
|
}
|
|
|
|
function normalizePathForComparison(value: string, compareAsWindows: boolean): string {
|
|
const platformPath = compareAsWindows ? nodePath.win32 : nodePath.posix;
|
|
const comparableValue = compareAsWindows ? stripWindowsNamespacePrefix(value) : value;
|
|
const platformNormalized = platformPath.normalize(comparableValue);
|
|
const normalized = stripTrailingSeparators(
|
|
platformNormalized,
|
|
platformPath.parse(platformNormalized).root,
|
|
compareAsWindows,
|
|
);
|
|
return compareAsWindows ? normalized.toLowerCase() : normalized;
|
|
}
|
|
|
|
function stripWindowsNamespacePrefix(value: string): string {
|
|
const driveMatch = value.match(/^[/\\]{2}\?[/\\]([a-zA-Z]:)[/\\](.*)$/u);
|
|
const drivePrefix = driveMatch?.[1];
|
|
if (drivePrefix) {
|
|
return `${drivePrefix}\\${driveMatch[2] ?? ""}`;
|
|
}
|
|
|
|
const uncMatch = value.match(/^[/\\]{2}\?[/\\]UNC[/\\]([^/\\]+)[/\\]([^/\\]+)(?:[/\\](.*))?$/iu);
|
|
const uncServer = uncMatch?.[1];
|
|
const uncShare = uncMatch?.[2];
|
|
if (uncServer && uncShare) {
|
|
const uncRest = uncMatch[3];
|
|
return `\\\\${uncServer}\\${uncShare}${uncRest !== undefined ? `\\${uncRest}` : ""}`;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
function stripTrailingSeparators(value: string, root: string, compareAsWindows: boolean): string {
|
|
const separatorPattern = compareAsWindows ? /[\\/]/u : /\//u;
|
|
let result = value;
|
|
while (result.length > root.length && separatorPattern.test(result.at(-1) ?? "")) {
|
|
result = result.slice(0, -1);
|
|
}
|
|
return result;
|
|
}
|