Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
22 kB · 637 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638import { createHash } from "crypto";import { accessSync, appendFileSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync,} from "fs";import { homedir } from "os";import { basename, dirname, join, posix, resolve, sep, win32 } from "path";import { fileURLToPath } from "url";import { shouldUseWindowsShell, spawnSyncHidden } from "./utils/child-process.js";import { normalizeSocketPath } from "./utils/daemon-socket-path.js";import { getNativeInstallationTarget } from "./utils/native-installation.js";
// =============================================================================// Package Detection// =============================================================================
const __filename = fileURLToPath(import.meta.url);const __dirname = dirname(__filename);
/** * Detect if we're running as a Bun compiled binary. * Bun binaries have import.meta.url containing "$bunfs", "~BUN", or "%7EBUN" (Bun's virtual filesystem path) */export const isBunBinary = import.meta.url.includes("$bunfs") || import.meta.url.includes("~BUN") || import.meta.url.includes("%7EBUN");
/** Detect if Bun is the runtime (compiled binary or bun run) */export const isBunRuntime = !!process.versions.bun;
export const SELF_UPDATE_INTERACTIVE_CHILD_ENV = "PRIME_AGENT_INTERACTIVE_SELF_UPDATE";export const SELF_UPDATE_NOT_ATTEMPTED_EXIT_CODE = 75;
// =============================================================================// Install Method Detection// =============================================================================
export type InstallMethod = "bun-binary" | "homebrew" | "npm" | "pnpm" | "yarn" | "bun" | "unknown";
interface SelfUpdateCommandStep { command: string; args: string[]; display: string;}
export interface SelfUpdateCommand extends SelfUpdateCommandStep { steps?: SelfUpdateCommandStep[];}
function makeSelfUpdateCommand( installStep: SelfUpdateCommandStep, uninstallStep?: SelfUpdateCommandStep, options: { uninstallAfterInstall?: boolean } = {},): SelfUpdateCommand { if (!uninstallStep) return installStep; if (options.uninstallAfterInstall) { return { ...installStep, display: `${installStep.display} && ${uninstallStep.display}`, steps: [installStep, uninstallStep], }; } return { ...installStep, display: `${uninstallStep.display} && ${installStep.display}`, steps: [uninstallStep, installStep], };}
function makeSelfUpdateCommandStep(command: string, args: string[]): SelfUpdateCommandStep { return { command, args, display: [command, ...args].map((arg) => (/\s/.test(arg) ? `"${arg}"` : arg)).join(" "), };}
export function detectInstallMethod(): InstallMethod { if (isBunBinary) { return "bun-binary"; } if (isHomebrewInstall()) { return "homebrew"; }
const resolvedPath = `${__dirname}\0${process.execPath || ""}`.toLowerCase().replace(/\\/g, "/");
if (resolvedPath.includes("/pnpm/") || resolvedPath.includes("/.pnpm/")) { return "pnpm"; } if (resolvedPath.includes("/yarn/") || resolvedPath.includes("/.yarn/")) { return "yarn"; } if (isBunRuntime || resolvedPath.includes("/install/global/node_modules/")) { return "bun"; } if (resolvedPath.includes("/npm/") || resolvedPath.includes("/node_modules/")) { return "npm"; }
return "unknown";}
function isHomebrewInstall(): boolean { const packageDir = getPackageDir().toLowerCase().replace(/\\/g, "/"); return packageDir.includes("/cellar/") && packageDir.includes("/libexec/lib/node_modules/");}
function getInferredNpmInstall(): { root: string; prefix: string } | undefined { const packageDir = getPackageDir(); const path = process.platform === "win32" || packageDir.includes("\\") ? win32 : { basename, dirname }; const parent = path.dirname(packageDir); let root: string | undefined; if (path.basename(parent).startsWith("@") && path.basename(path.dirname(parent)) === "node_modules") { root = path.dirname(parent); } else if (path.basename(parent) === "node_modules") { root = parent; } if (!root) return undefined; const rootParent = path.dirname(root); if (path.basename(rootParent) === "lib") return { root, prefix: path.dirname(rootParent) }; // Windows global npm prefixes use `<prefix>\\node_modules`, which is // indistinguishable from local project installs by path shape alone. Do not // infer unsupported Windows custom prefixes without `npm root -g` evidence. return undefined;}
function isDirectPackageArtifactSpec(updateSpec: string): boolean { const spec = updateSpec.trim().toLowerCase(); return ( spec.startsWith("http://") || spec.startsWith("https://") || spec.startsWith("file:") || spec.endsWith(".tgz") || spec.endsWith(".tar.gz") );}
function getDefaultUpdatePackageName(installedPackageName: string, updateSpec: string): string { if (isDirectPackageArtifactSpec(updateSpec)) { return installedPackageName; } return updateSpec;}
function getSelfUpdateCommandForMethod( method: InstallMethod, installedPackageName: string, updateSpec = installedPackageName, npmCommand?: string[], updatePackageName = getDefaultUpdatePackageName(installedPackageName, updateSpec),): SelfUpdateCommand | undefined { const uninstallAfterInstall = isDirectPackageArtifactSpec(updateSpec); switch (method) { case "bun-binary": case "homebrew": return undefined; case "pnpm": return makeSelfUpdateCommand( makeSelfUpdateCommandStep("pnpm", ["install", "-g", updateSpec]), updatePackageName === installedPackageName ? undefined : makeSelfUpdateCommandStep("pnpm", ["remove", "-g", installedPackageName]), { uninstallAfterInstall }, ); case "yarn": return makeSelfUpdateCommand( makeSelfUpdateCommandStep("yarn", ["global", "add", updateSpec]), updatePackageName === installedPackageName ? undefined : makeSelfUpdateCommandStep("yarn", ["global", "remove", installedPackageName]), { uninstallAfterInstall }, ); case "bun": return makeSelfUpdateCommand( makeSelfUpdateCommandStep("bun", ["install", "-g", updateSpec]), updatePackageName === installedPackageName ? undefined : makeSelfUpdateCommandStep("bun", ["uninstall", "-g", installedPackageName]), { uninstallAfterInstall }, ); case "npm": { const [command = "npm", ...npmArgs] = npmCommand ?? []; const inferred = npmCommand?.length ? undefined : getInferredNpmInstall(); const prefixArgs = [...npmArgs, ...(inferred ? ["--prefix", inferred.prefix] : [])]; const installStep = makeSelfUpdateCommandStep(command, [...prefixArgs, "install", "-g", updateSpec]); const uninstallStep = updatePackageName === installedPackageName ? undefined : makeSelfUpdateCommandStep(command, [...prefixArgs, "uninstall", "-g", installedPackageName]); return makeSelfUpdateCommand(installStep, uninstallStep, { uninstallAfterInstall }); } case "unknown": return undefined; }}
function readCommandOutput( command: string, args: string[], options: { requireSuccess?: boolean } = {},): string | undefined { const result = spawnSyncHidden(command, args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], shell: shouldUseWindowsShell(command), }); if (result.status === 0) return result.stdout.trim() || undefined; if (options.requireSuccess) { const reason = result.error?.message || result.stderr.trim() || `exit code ${result.status ?? "unknown"}`; throw new Error(`Failed to run ${[command, ...args].join(" ")}: ${reason}`); } return undefined;}
function getGlobalPackageRoots(method: InstallMethod, _packageName: string, npmCommand?: string[]): string[] { switch (method) { case "npm": { const configured = !!npmCommand?.length; const [command = "npm", ...npmArgs] = npmCommand ?? []; if (configured && command === "bun") { const bunBin = readCommandOutput(command, [...npmArgs, "pm", "bin", "-g"], { requireSuccess: true, }); const roots = [join(homedir(), ".bun", "install", "global", "node_modules")]; if (bunBin) { roots.push(join(dirname(bunBin), "install", "global", "node_modules")); } return roots; } const root = readCommandOutput(command, [...npmArgs, "root", "-g"], { requireSuccess: configured, }); const inferred = configured ? undefined : getInferredNpmInstall(); return [root, inferred?.root].filter((x): x is string => !!x); } case "pnpm": { const root = readCommandOutput("pnpm", ["root", "-g"]); return root ? [root, dirname(root)] : []; } case "yarn": { const dir = readCommandOutput("yarn", ["global", "dir"]); return dir ? [dir, join(dir, "node_modules")] : []; } case "bun": { const bunBin = readCommandOutput("bun", ["pm", "bin", "-g"]); const roots = [join(homedir(), ".bun", "install", "global", "node_modules")]; if (bunBin) { roots.push(join(dirname(bunBin), "install", "global", "node_modules")); } return roots; } case "bun-binary": case "homebrew": case "unknown": return []; }}
function normalizeExistingPathForComparison(path: string): string | undefined { const resolvedPath = resolve(path); if (!existsSync(resolvedPath)) { return undefined; } let normalizedPath: string; try { normalizedPath = realpathSync(resolvedPath); } catch { return undefined; } if (process.platform === "win32") { normalizedPath = normalizedPath.toLowerCase(); } return normalizedPath;}
function isSelfUpdatePathWritable(): boolean { const packageDir = getPackageDir(); try { accessSync(packageDir, constants.W_OK); accessSync(dirname(packageDir), constants.W_OK); return true; } catch { return false; }}
function isManagedByGlobalPackageManager(method: InstallMethod, packageName: string, npmCommand?: string[]): boolean { const packageDir = normalizeExistingPathForComparison(getPackageDir()); return ( !!packageDir && getGlobalPackageRoots(method, packageName, npmCommand).some((root) => { const normalizedRoot = normalizeExistingPathForComparison(root); return ( !!normalizedRoot && packageDir.startsWith(normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`) ); }) );}
export function getSelfUpdateCommand( packageName: string, npmCommand?: string[], updateSpec = packageName, updatePackageName = getDefaultUpdatePackageName(packageName, updateSpec),): SelfUpdateCommand | undefined { const method = detectInstallMethod(); const command = getSelfUpdateCommandForMethod(method, packageName, updateSpec, npmCommand, updatePackageName); if (!command || !isManagedByGlobalPackageManager(method, packageName, npmCommand) || !isSelfUpdatePathWritable()) { return undefined; } return command;}
export function getSelfUpdateUnavailableInstruction( packageName: string, npmCommand?: string[], updateSpec = packageName, updatePackageName = getDefaultUpdatePackageName(packageName, updateSpec),): string { const method = detectInstallMethod(); if (method === "bun-binary") { return `Download from: https://github.com/PrimeIntellect-ai/prime-agent/releases/latest`; } if (method === "homebrew") { return `Update with: brew upgrade ${APP_NAME}`; } const command = getSelfUpdateCommandForMethod(method, packageName, updateSpec, npmCommand, updatePackageName); if (command) { if (isManagedByGlobalPackageManager(method, packageName, npmCommand) && !isSelfUpdatePathWritable()) { return `This installation is managed by a global ${method} install, but the install path is not writable. Update it yourself with: ${command.display}`; } return `This installation is not managed by a global ${method} install. Update it with the package manager, wrapper, or source checkout that provides it.`; } return `Update ${updateSpec} using the package manager, wrapper, or source checkout that provides this installation.`;}
export function getUpdateInstruction(packageName: string): string { if (isBunBinary && getNativeInstallationTarget()) return `Run: ${APP_NAME} update`; const method = detectInstallMethod(); const command = getSelfUpdateCommandForMethod(method, packageName); if (command) { return `Run: ${command.display}`; } return getSelfUpdateUnavailableInstruction(packageName);}
// =============================================================================// Package Asset Paths (shipped with executable)// =============================================================================
/** * Get the base directory for resolving package assets (themes, package.json, README.md, CHANGELOG.md). * - For Bun binary: returns the directory containing the executable * - For Node.js (dist/): returns __dirname (the dist/ directory) * - For tsx (src/): returns parent directory (the package root) */export function getPackageDir(): string { // Allow override via environment variable (useful for Nix/Guix where store paths tokenize poorly) const envDir = process.env.PI_PACKAGE_DIR; if (envDir) { return expandTildePath(envDir); }
if (isBunBinary) { // Bun binary: process.execPath points to the compiled executable return dirname(process.execPath); } // Node.js: walk up from __dirname until we find package.json let dir = __dirname; while (dir !== dirname(dir)) { if (existsSync(join(dir, "package.json"))) { return dir; } dir = dirname(dir); } // Fallback (shouldn't happen) return __dirname;}
/** * Get path to built-in themes directory (shipped with package) * - For Bun binary: theme/ next to executable * - For Node.js (dist/): dist/modes/interactive/theme/ * - For tsx (src/): src/modes/interactive/theme/ */export function getThemesDir(): string { if (isBunBinary) { return join(getPackageDir(), "theme"); } // Theme is in modes/interactive/theme/ relative to src/ or dist/ const packageDir = getPackageDir(); const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist"; return join(packageDir, srcOrDist, "modes", "interactive", "theme");}
/** * Get path to HTML export template directory (shipped with package) * - For Bun binary: export-html/ next to executable * - For Node.js (dist/): dist/core/export-html/ * - For tsx (src/): src/core/export-html/ */export function getExportTemplateDir(): string { if (isBunBinary) { return join(getPackageDir(), "export-html"); } const packageDir = getPackageDir(); const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist"; return join(packageDir, srcOrDist, "core", "export-html");}
/** Get path to package.json */export function getPackageJsonPath(): string { return join(getPackageDir(), "package.json");}
/** Get path to docs directory */export function getDocsPath(): string { return resolve(join(getPackageDir(), "docs"));}
/** Get path to CHANGELOG.md */export function getChangelogPath(): string { return resolve(join(getPackageDir(), "CHANGELOG.md"));}
/** * Get path to built-in interactive assets directory. * - For Bun binary: assets/ next to executable * - For Node.js (dist/): dist/modes/interactive/assets/ * - For tsx (src/): src/modes/interactive/assets/ */export function getInteractiveAssetsDir(): string { if (isBunBinary) { return join(getPackageDir(), "assets"); } const packageDir = getPackageDir(); const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist"; return join(packageDir, srcOrDist, "modes", "interactive", "assets");}
/** Get path to a bundled interactive asset */export function getBundledInteractiveAssetPath(name: string): string { return join(getInteractiveAssetsDir(), name);}
/** * Get the directory containing built-in skills shipped with the package. * - For Bun binary: skills/ next to executable * - For Node.js (dist/): dist/skills/ * - For tsx (src/): skills/ at the package root */export function getBundledSkillsDir(): string { if (isBunBinary) { return join(getPackageDir(), "skills"); } const packageDir = getPackageDir(); // Source checkouts (tsx) keep built-in skills at the package root; built // packages copy them to dist/skills. Decide by whether src/ is present so a // stale dist/ from a prior build never shadows live source edits. const isSourceCheckout = existsSync(join(packageDir, "src")); return isSourceCheckout ? join(packageDir, "skills") : join(packageDir, "dist", "skills");}
// =============================================================================// App Config (from package.json piConfig)// =============================================================================
interface PackageJson { name?: string; version?: string; piConfig?: { name?: string; configDir?: string; };}
const pkg = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8")) as PackageJson;
const piConfigName: string | undefined = pkg.piConfig?.name;const envPrefix = (piConfigName || "pi") .toUpperCase() .replace(/[^A-Z0-9]+/g, "_") .replace(/^_+|_+$/g, "") || "PI";export const PACKAGE_NAME: string = pkg.name || "@earendil-works/pi-coding-agent";export const APP_NAME: string = piConfigName || "pi";export const APP_TITLE: string = piConfigName ? APP_NAME : "π";export const CONFIG_DIR_NAME: string = pkg.piConfig?.configDir || ".prime/agent";export const VERSION: string = pkg.version || "0.0.0";
// e.g., PI_CODING_AGENT_DIR or PRIME_AGENT_CODING_AGENT_DIRexport const ENV_AGENT_DIR = `${envPrefix}_CODING_AGENT_DIR`;export const ENV_SESSION_DIR = `${envPrefix}_SESSION_DIR`;export const ENV_LEGACY_SESSION_DIR = `${envPrefix}_CODING_AGENT_SESSION_DIR`;
export function expandTildePath(path: string, platform: NodeJS.Platform = process.platform): string { if (path === "~") return homedir(); if (path.startsWith("~/") || (platform === "win32" && path.startsWith("~\\"))) { return (platform === "win32" ? win32 : posix).join(homedir(), path.slice(2)); } return path;}
const DEFAULT_SHARE_VIEWER_URL = "https://pi.dev/session/";
/** Get the share viewer URL for a gist ID */export function getShareViewerUrl(gistId: string): string { const baseUrl = process.env.PI_SHARE_VIEWER_URL || DEFAULT_SHARE_VIEWER_URL; return `${baseUrl}#${gistId}`;}
// =============================================================================// User Config Paths (~/.prime/agent/*)// =============================================================================
/** Get the agent config directory (e.g., ~/.prime/agent/) */export function getAgentDir(): string { const envDir = process.env[ENV_AGENT_DIR]; if (envDir) { return expandTildePath(envDir); } return join(homedir(), CONFIG_DIR_NAME);}
/** Get path to user's custom themes directory */export function getCustomThemesDir(): string { return join(getAgentDir(), "themes");}
/** Directory where daemon and client diagnostic logs are written (e.g. ~/.prime/agent/logs/). */export function getLogsDir(): string { return join(getAgentDir(), "logs");}
/** Log file capturing client-side agent-open failures. */export function getClientErrorLogPath(): string { return join(getLogsDir(), "client-errors.log");}
export function getAgentTracesLogPath(): string { return join(getLogsDir(), "agent-traces.log");}
/** Shared structured (JSON lines) log for client, daemon, and provider diagnostics. */export function getAgentLogPath(): string { return join(getLogsDir(), "agent.jsonl");}
/** * Log file for a daemon. The basename keeps it readable; a hash of the full * socket path makes it unique so two sockets that share a basename (e.g. * daemon.sock in different dirs) don't interleave into one file. */export function getDaemonLogPath(socketPath: string): string { const normalized = normalizeSocketPath(socketPath); const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 8); return join(getLogsDir(), `${basename(normalized)}.${hash}.log`);}
export function getDaemonUpdateRestartManifestPath(socketPath: string, agentDir: string = getAgentDir()): string { const normalizedSocketPath = normalizeSocketPath(socketPath); const socketHash = createHash("sha256").update(normalizedSocketPath).digest("hex"); return join(agentDir, "daemon-update-restarts", `${socketHash}.json`);}
export function getLegacyDaemonUpdateRestartManifestPath(agentDir: string = getAgentDir()): string { return join(agentDir, "daemon-update-restart.json");}
const MAX_LOG_BYTES = 5 * 1024 * 1024;
/** * Append a line to a log file, keeping its size bounded with a single-generation * rotation. Opens and closes per call (no held fd), so rotation works at runtime * — a long-lived writer rotates on the write that crosses the cap, not only at * startup. Best-effort: diagnostics must never throw into the caller. */export function appendRotatingLog(logPath: string, message: string, maxBytes: number = MAX_LOG_BYTES): void { try { mkdirSync(dirname(logPath), { recursive: true }); try { if (existsSync(logPath) && statSync(logPath).size > maxBytes) { // Drop any prior .old first: renameSync fails on Windows if it exists. rmSync(`${logPath}.old`, { force: true }); renameSync(logPath, `${logPath}.old`); } } catch { // Keep appending rather than dropping the log on a rotation failure. } appendFileSync(logPath, `${message}\n`); } catch { // A read-only or missing log dir must never break the caller. }}
/** Get path to auth.json */export function getAuthPath(): string { return join(getAgentDir(), "auth.json");}
/** Get path to cron jobs store */export function getCronJobsPath(agentDir: string = getAgentDir()): string { return join(agentDir, "cron-jobs.json");}
/** Get path to managed binaries directory (fd, rg) */export function getBinDir(): string { return join(getAgentDir(), "bin");}
/** Get path to sessions directory */export function getSessionsDir(agentDir: string = getAgentDir()): string { const envDir = getSessionDirEnvOverride(); if (envDir) { return envDir; } return join(agentDir, "sessions");}
export function getSessionDirEnvOverride(): string | undefined { const envDir = process.env[ENV_SESSION_DIR] ?? process.env[ENV_LEGACY_SESSION_DIR]; return envDir ? expandTildePath(envDir) : undefined;}
/** Get path to debug log file */export function getDebugLogPath(): string { return join(getAgentDir(), `${APP_NAME}-debug.log`);}