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.
13 kB · 454 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455/** * One-time migrations that run on startup. */
import chalk from "chalk";import { type Dirent, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmdirSync, rmSync, statSync, writeFileSync,} from "fs";import { basename, dirname, join } from "path";import { CONFIG_DIR_NAME, getAgentDir, getBinDir, getSessionsDir } from "./config.js";import { migrateKeybindingsConfig } from "./core/keybindings.js";import { realpathIfPresentSync, writeFileAtomicSync } from "./utils/atomic-file.js";import { readFirstLineSync } from "./utils/file-lines.js";
const MIGRATION_GUIDE_URL = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md#extensions-migration";const EXTENSIONS_DOC_URL = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/docs/extensions.md";
/** * Migrate legacy oauth.json and settings.json apiKeys to auth.json. * * @returns Array of provider names that were migrated */export function migrateAuthToAuthJson(): string[] { const agentDir = getAgentDir(); const authPath = join(agentDir, "auth.json"); const oauthPath = join(agentDir, "oauth.json"); const settingsPath = join(agentDir, "settings.json");
// Skip if auth.json already exists if (existsSync(authPath)) return [];
const migrated: Record<string, unknown> = {}; const providers: string[] = [];
let oauthReadable = false; if (existsSync(oauthPath)) { try { const oauth = JSON.parse(readFileSync(oauthPath, "utf-8")); for (const [provider, cred] of Object.entries(oauth)) { migrated[provider] = { type: "oauth", ...(cred as object) }; providers.push(provider); } oauthReadable = true; } catch { // Skip on error } }
let settingsWithoutApiKeys: string | undefined; let settingsMode: number | undefined; if (existsSync(settingsPath)) { try { settingsMode = statSync(settingsPath).mode & 0o777; const settings = JSON.parse(readFileSync(settingsPath, "utf-8")); if (settings.apiKeys && typeof settings.apiKeys === "object") { for (const [provider, key] of Object.entries(settings.apiKeys)) { if (!migrated[provider] && typeof key === "string") { migrated[provider] = { type: "api_key", key }; providers.push(provider); } } delete settings.apiKeys; settingsWithoutApiKeys = JSON.stringify(settings, null, 2); } } catch { // Skip on error } }
// The destination must be durable before any source is destroyed. if (Object.keys(migrated).length > 0) { mkdirSync(dirname(authPath), { recursive: true }); writeFileAtomicSync(realpathIfPresentSync(authPath), JSON.stringify(migrated, null, 2), { mode: 0o600, fsync: true, fsyncDir: true, }); } // Source cleanup is best-effort: with auth.json durable, leftovers are inert. try { if (oauthReadable) { renameSync(oauthPath, `${oauthPath}.migrated`); } } catch { // Skip on error } try { if (settingsWithoutApiKeys !== undefined) { writeFileAtomicSync( realpathIfPresentSync(settingsPath), settingsWithoutApiKeys, settingsMode === undefined ? {} : { mode: settingsMode }, ); } } catch { // Skip on error }
return providers;}
/** * Migrate sessions from ~/.pi/agent/*.jsonl to the session root. * * Bug in v0.30.0: Sessions were saved to ~/.pi/agent/ instead of * ~/.pi/agent/sessions/. This migration moves them to the configured * session root. * * See: https://github.com/earendil-works/pi-mono/issues/320 */export function migrateSessionsFromAgentRoot(): void { const agentDir = getAgentDir();
// Find all .jsonl files directly in agentDir (not in subdirectories) let files: string[]; try { files = readdirSync(agentDir) .filter((f) => f.endsWith(".jsonl")) .map((f) => join(agentDir, f)); } catch { return; }
if (files.length === 0) return;
for (const file of files) { try { // Read first line to get session header const firstLine = readFirstLineSync(file); if (!firstLine?.trim()) continue;
const header = JSON.parse(firstLine); if (header.type !== "session") continue;
const correctDir = getSessionsDir(agentDir);
// Create directory if needed if (!existsSync(correctDir)) { mkdirSync(correctDir, { recursive: true }); }
// Move the file const newPath = join(correctDir, basename(file));
if (existsSync(newPath)) continue; // Skip if target exists
renameSync(file, newPath); } catch { // Skip files that can't be migrated } }}
function isSessionJsonlFile(filePath: string): boolean { try { const firstLine = readFirstLineSync(filePath); if (!firstLine?.trim()) { return false; } const header = JSON.parse(firstLine) as { type?: unknown; id?: unknown }; return header.type === "session" && typeof header.id === "string"; } catch { return false; }}
function isLegacySessionDirName(name: string): boolean { return /^--.+--$/.test(name);}
/** * Migrate legacy per-cwd session directories into the flat session root. * * Older versions stored sessions under ~/.prime/agent/sessions/--cwd--/*.jsonl. * The daemon list/continue paths now scan the flat session root, so move any * existing nested JSONL session files up one level. */export function migrateLegacySessionDirsToSessionRoot(): void { const agentDir = getAgentDir(); const sessionsDir = getSessionsDir(agentDir);
let entries: Dirent[]; try { entries = readdirSync(sessionsDir, { withFileTypes: true }); } catch { return; }
for (const entry of entries) { if (!entry.isDirectory() || !isLegacySessionDirName(entry.name)) { continue; }
const legacyDir = join(sessionsDir, entry.name); let files: string[]; try { files = readdirSync(legacyDir).filter((file) => file.endsWith(".jsonl")); } catch { continue; }
for (const file of files) { const oldPath = join(legacyDir, file); let newPath = join(sessionsDir, file); if (!isSessionJsonlFile(oldPath)) { continue; } if (existsSync(newPath)) { if (filesHaveSameContent(oldPath, newPath)) { // Already migrated; leave the legacy copy alone. continue; } // A different session shares the basename; move it under a unique name // so it stays discoverable by the flat-root list and continue paths. newPath = uniqueSessionRootPath(sessionsDir, file); } try { renameSync(oldPath, newPath); } catch { // Leave the legacy file in place if it cannot be moved. } }
try { if (readdirSync(legacyDir).length === 0) { rmdirSync(legacyDir); } } catch { // Ignore cleanup errors; migrated files are already in the flat root. } }}
function filesHaveSameContent(a: string, b: string): boolean { try { if (statSync(a).size !== statSync(b).size) { return false; } return readFileSync(a, "utf-8") === readFileSync(b, "utf-8"); } catch { return false; }}
function uniqueSessionRootPath(sessionsDir: string, file: string): string { const base = file.endsWith(".jsonl") ? file.slice(0, -".jsonl".length) : file; for (let n = 1; ; n++) { const candidate = join(sessionsDir, `${base}-${n}.jsonl`); if (!existsSync(candidate)) { return candidate; } }}
/** * Migrate commands/ to prompts/ if needed. * Works for both regular directories and symlinks. */function migrateCommandsToPrompts(baseDir: string, label: string): boolean { const commandsDir = join(baseDir, "commands"); const promptsDir = join(baseDir, "prompts");
if (existsSync(commandsDir) && !existsSync(promptsDir)) { try { renameSync(commandsDir, promptsDir); console.log(chalk.green(`Migrated ${label} commands/ → prompts/`)); return true; } catch (err) { console.log( chalk.yellow( `Warning: Could not migrate ${label} commands/ to prompts/: ${err instanceof Error ? err.message : err}`, ), ); } } return false;}
function migrateKeybindingsConfigFile(): void { const configPath = join(getAgentDir(), "keybindings.json"); if (!existsSync(configPath)) return;
try { const parsed = JSON.parse(readFileSync(configPath, "utf-8")) as unknown; if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { return; } const { config, migrated } = migrateKeybindingsConfig(parsed as Record<string, unknown>); if (!migrated) return; writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8"); } catch { // Ignore malformed files during migration }}
/** * Move fd/rg binaries from tools/ to bin/ if they exist. */function migrateToolsToBin(): void { const agentDir = getAgentDir(); const toolsDir = join(agentDir, "tools"); const binDir = getBinDir();
if (!existsSync(toolsDir)) return;
const binaries = ["fd", "rg", "fd.exe", "rg.exe"]; let movedAny = false;
for (const bin of binaries) { const oldPath = join(toolsDir, bin); const newPath = join(binDir, bin);
if (existsSync(oldPath)) { if (!existsSync(binDir)) { mkdirSync(binDir, { recursive: true }); } if (!existsSync(newPath)) { try { renameSync(oldPath, newPath); movedAny = true; } catch { // Ignore errors } } else { // Target exists, just delete the old one try { rmSync?.(oldPath, { force: true }); } catch { // Ignore } } } }
if (movedAny) { console.log(chalk.green(`Migrated managed binaries tools/ → bin/`)); }}
/** * Check for deprecated hooks/ and tools/ directories. * Note: tools/ may contain fd/rg binaries extracted by pi, so only warn if it has other files. */function checkDeprecatedExtensionDirs(baseDir: string, label: string): string[] { const hooksDir = join(baseDir, "hooks"); const toolsDir = join(baseDir, "tools"); const warnings: string[] = [];
if (existsSync(hooksDir)) { warnings.push(`${label} hooks/ directory found. Hooks have been renamed to extensions.`); }
if (existsSync(toolsDir)) { // Check if tools/ contains anything other than fd/rg (which are auto-extracted binaries) try { const entries = readdirSync(toolsDir); const customTools = entries.filter((e) => { const lower = e.toLowerCase(); return ( lower !== "fd" && lower !== "rg" && lower !== "fd.exe" && lower !== "rg.exe" && !e.startsWith(".") // Ignore .DS_Store and other hidden files ); }); if (customTools.length > 0) { warnings.push( `${label} tools/ directory contains custom tools. Custom tools have been merged into extensions.`, ); } } catch { // Ignore read errors } }
return warnings;}
/** * Run extension system migrations (commands→prompts) and collect warnings about deprecated directories. */function migrateExtensionSystem(cwd: string): string[] { const agentDir = getAgentDir(); const projectDir = join(cwd, CONFIG_DIR_NAME);
// Migrate commands/ to prompts/ migrateCommandsToPrompts(agentDir, "Global"); migrateCommandsToPrompts(projectDir, "Project");
// Check for deprecated directories const warnings = [ ...checkDeprecatedExtensionDirs(agentDir, "Global"), ...checkDeprecatedExtensionDirs(projectDir, "Project"), ];
return warnings;}
/** * Print deprecation warnings and wait for keypress. */export async function showDeprecationWarnings(warnings: string[]): Promise<void> { if (warnings.length === 0) return;
for (const warning of warnings) { console.log(chalk.yellow(`Warning: ${warning}`)); } console.log(chalk.yellow(`\nMove your extensions to the extensions/ directory.`)); console.log(chalk.yellow(`Migration guide: ${MIGRATION_GUIDE_URL}`)); console.log(chalk.yellow(`Documentation: ${EXTENSIONS_DOC_URL}`)); console.log(chalk.dim(`\nPress any key to continue...`));
if (!process.stdin.isTTY) { // A non-interactive parent never delivers the keypress; don't hang waiting for it. console.log(); return; } await new Promise<void>((resolve) => { process.stdin.setRawMode?.(true); process.stdin.resume(); process.stdin.once("data", () => { process.stdin.setRawMode?.(false); process.stdin.pause(); resolve(); }); }); console.log();}
/** * Run all migrations. Called once on startup. * * @returns Object with migration results and deprecation warnings */export function runMigrations(cwd: string): { migratedAuthProviders: string[]; deprecationWarnings: string[];} { const migratedAuthProviders = migrateAuthToAuthJson(); migrateSessionsFromAgentRoot(); migrateLegacySessionDirsToSessionRoot(); migrateToolsToBin(); migrateKeybindingsConfigFile(); const deprecationWarnings = migrateExtensionSystem(cwd); return { migratedAuthProviders, deprecationWarnings };}