Files
oh-my-pi/packages/utils/src/dirs.ts
T
can1357 3293231a9f feat(coding-agent): implemented sqlite-backed storage for autoresearch
- Replaced file-backed autoresearch contracts with sqlite-backed session/run storage in `~/.omp/autoresearch`.
- Added `AutoresearchStorage` and rewired `init_experiment`, `run_experiment`, and `log_experiment` to persist sessions and runs.
- Added `update_notes` tool with `body`/`append_idea` inputs and updated prompts to use active-session context.
- Removed `autoresearch.md` contract parsing and checks flow, including `runChecks`, `force`, and timeout schema options.
- Updated autoresearch state/types to persist `goal`, `notes`, `branch`, and `baselineCommit` plus run justification/flag metadata.
2026-05-02 01:10:00 +02:00

446 lines
16 KiB
TypeScript

/**
* Centralized path helpers for omp config directories.
*
* Uses PI_CONFIG_DIR (default ".omp") for the config root and
* PI_CODING_AGENT_DIR to override the agent directory.
*
* On Linux, if XDG_DATA_HOME / XDG_STATE_HOME / XDG_CACHE_HOME environment
* variables are set, paths are redirected to XDG-compliant locations under
* $XDG_*_HOME/omp/. This requires running `omp config migrate` first to
* move data to the new locations. No filesystem existence checks are performed
* — if the env var is set, omp trusts that the migration has been done.
*/
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { engines, version } from "../package.json" with { type: "json" };
/** App name (e.g. "omp") */
export const APP_NAME: string = "omp";
/** Config directory name (e.g. ".omp") */
export const CONFIG_DIR_NAME: string = ".omp";
/** Version (e.g. "1.0.0") */
export const VERSION: string = version;
/** Minimum Bun version */
export const MIN_BUN_VERSION: string = engines.bun.replace(/[^0-9.]/g, "");
// =============================================================================
// Project directory
// =============================================================================
/**
* On macOS, strip /private prefix only when both paths resolve to the same location.
* This preserves aliases like /private/tmp -> /tmp without rewriting unrelated paths.
*/
function standardizeMacOSPath(p: string): string {
if (process.platform !== "darwin" || !p.startsWith("/private/")) return p;
const stripped = p.slice("/private".length);
try {
if (fs.realpathSync(p) === fs.realpathSync(stripped)) {
return stripped;
}
} catch {}
return p;
}
export function resolveEquivalentPath(inputPath: string): string {
const resolvedPath = path.resolve(inputPath);
try {
return fs.realpathSync(resolvedPath);
} catch {
return resolvedPath;
}
}
export function normalizePathForComparison(inputPath: string): string {
const resolvedPath = resolveEquivalentPath(inputPath);
return process.platform === "win32" ? resolvedPath.toLowerCase() : resolvedPath;
}
export function pathIsWithin(root: string, candidate: string): boolean {
const normalizedRoot = normalizePathForComparison(root);
const normalizedCandidate = normalizePathForComparison(candidate);
const relative = path.relative(normalizedRoot, normalizedCandidate);
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
export function relativePathWithinRoot(root: string, candidate: string): string | null {
if (!pathIsWithin(root, candidate)) return null;
const normalizedRoot = normalizePathForComparison(root);
const normalizedCandidate = normalizePathForComparison(candidate);
const relative = path.relative(normalizedRoot, normalizedCandidate);
return relative || null;
}
let projectDir = standardizeMacOSPath(process.cwd());
/** Get the project directory. */
export function getProjectDir(): string {
return projectDir;
}
/** Set the project directory. */
export function setProjectDir(dir: string): void {
projectDir = standardizeMacOSPath(path.resolve(dir));
process.chdir(projectDir);
}
/** Get the config directory name relative to home (e.g. ".omp" or PI_CONFIG_DIR override). */
export function getConfigDirName(): string {
return process.env.PI_CONFIG_DIR || CONFIG_DIR_NAME;
}
/** Get the config agent directory name relative to home (e.g. ".omp/agent" or PI_CONFIG_DIR + "/agent"). */
export function getConfigAgentDirName(): string {
return `${getConfigDirName()}/agent`;
}
// =============================================================================
// DirResolver — cached, XDG-aware path resolution
// =============================================================================
type XdgCategory = "data" | "state" | "cache";
/**
* Resolves and caches all omp directory paths. On Linux, when XDG environment
* variables are set, paths are redirected under $XDG_*_HOME/omp/. A new
* instance is created whenever the agent directory changes, which naturally
* invalidates all cached paths.
*/
class DirResolver {
readonly configRoot: string;
readonly agentDir: string;
// Per-category base dirs. Without XDG, all three equal configRoot / agentDir.
// With XDG on Linux, they point to $XDG_*_HOME/omp/.
readonly #rootDirs: Record<XdgCategory, string>;
readonly #agentDirs: Record<XdgCategory, string>;
readonly #rootCache = new Map<string, string>();
readonly #agentCache = new Map<string, string>();
constructor(agentDirOverride?: string) {
this.configRoot = path.join(os.homedir(), getConfigDirName());
const defaultAgent = path.join(this.configRoot, "agent");
this.agentDir = agentDirOverride ? path.resolve(agentDirOverride) : defaultAgent;
const isDefault = this.agentDir === defaultAgent;
// XDG is a Linux convention. On other platforms, or for non-default
// profiles, all categories resolve to the legacy paths.
let xdgData: string | undefined;
let xdgState: string | undefined;
let xdgCache: string | undefined;
if ((process.platform === "linux" || process.platform === "darwin") && isDefault) {
const resolveIf = (envVar: string) => {
const value = process.env[envVar];
if (value) {
try {
const joined = path.join(value, APP_NAME);
if (fs.existsSync(joined)) {
return joined;
}
} catch {}
}
return undefined;
};
xdgData = resolveIf("XDG_DATA_HOME");
xdgState = resolveIf("XDG_STATE_HOME");
xdgCache = resolveIf("XDG_CACHE_HOME");
}
this.#rootDirs = {
data: xdgData ?? this.configRoot,
state: xdgState ?? this.configRoot,
cache: xdgCache ?? this.configRoot,
};
// XDG flattens the agent/ prefix: ~/.omp/agent/sessions → $XDG_DATA_HOME/omp/sessions
this.#agentDirs = {
data: xdgData ?? this.agentDir,
state: xdgState ?? this.agentDir,
cache: xdgCache ?? this.agentDir,
};
}
/** Config-root subdirectory, with optional XDG override. */
rootSubdir(subdir: string, xdg?: XdgCategory): string {
const cached = this.#rootCache.get(subdir);
if (cached) return cached;
const base = xdg ? this.#rootDirs[xdg] : this.configRoot;
const result = path.join(base, subdir);
this.#rootCache.set(subdir, result);
return result;
}
/** Agent subdirectory, with optional XDG override. */
agentSubdir(userAgentDir: string | undefined, subdir: string, xdg?: XdgCategory): string {
if (!userAgentDir || userAgentDir === this.agentDir) {
const cached = this.#agentCache.get(subdir);
if (cached) return cached;
const base = xdg ? this.#agentDirs[xdg] : this.agentDir;
const result = path.join(base, subdir);
this.#agentCache.set(subdir, result);
return result;
}
return path.join(userAgentDir, subdir);
}
}
let dirs = new DirResolver(process.env.PI_CODING_AGENT_DIR);
// =============================================================================
// Root directories
// =============================================================================
/** Get the config root directory (~/.omp). */
export function getConfigRootDir(): string {
return dirs.configRoot;
}
/** Set the coding agent directory. Creates a fresh resolver, invalidating all cached paths. */
export function setAgentDir(dir: string): void {
dirs = new DirResolver(dir);
process.env.PI_CODING_AGENT_DIR = dir;
}
/** Get the agent config directory (~/.omp/agent). */
export function getAgentDir(): string {
return dirs.agentDir;
}
/** Get the project-local config directory (.omp). */
export function getProjectAgentDir(cwd: string = getProjectDir()): string {
return path.join(cwd, CONFIG_DIR_NAME);
}
// =============================================================================
// Config-root subdirectories (~/.omp/*)
// =============================================================================
/** Get the reports directory (~/.omp/reports). */
export function getReportsDir(): string {
return dirs.rootSubdir("reports", "state");
}
/** Get the logs directory (~/.omp/logs). */
export function getLogsDir(): string {
return dirs.rootSubdir("logs", "state");
}
/** Get the path to a dated log file (~/.omp/logs/omp.YYYY-MM-DD.log). */
export function getLogPath(date = new Date()): string {
return path.join(getLogsDir(), `${APP_NAME}.${date.toISOString().slice(0, 10)}.log`);
}
/** Get the plugins directory (~/.omp/plugins). */
export function getPluginsDir(): string {
return dirs.rootSubdir("plugins", "data");
}
/** Where npm installs packages (~/.omp/plugins/node_modules). */
export function getPluginsNodeModules(): string {
return path.join(getPluginsDir(), "node_modules");
}
/** Plugin manifest (~/.omp/plugins/package.json). */
export function getPluginsPackageJson(): string {
return path.join(getPluginsDir(), "package.json");
}
/** Plugin lock file (~/.omp/plugins/omp-plugins.lock.json). */
export function getPluginsLockfile(): string {
return path.join(getPluginsDir(), "omp-plugins.lock.json");
}
/** Get the remote mount directory (~/.omp/remote). */
export function getRemoteDir(): string {
return dirs.rootSubdir("remote", "data");
}
/** Get the PR worktrees directory (~/.omp/wt). */
export function getWorktreesDir(): string {
return dirs.rootSubdir("wt", "data");
}
/** Get the SSH control socket directory (~/.omp/ssh-control). */
export function getSshControlDir(): string {
return dirs.rootSubdir("ssh-control", "state");
}
/** Get the remote host info directory (~/.omp/remote-host). */
export function getRemoteHostDir(): string {
return dirs.rootSubdir("remote-host", "data");
}
/** Get the managed Python venv directory (~/.omp/python-env). */
export function getPythonEnvDir(): string {
return dirs.rootSubdir("python-env", "data");
}
/** Get the puppeteer sandbox directory (~/.omp/puppeteer). */
export function getPuppeteerDir(): string {
return dirs.rootSubdir("puppeteer", "cache");
}
/** Get the worktree base directory (~/.omp/wt). */
export function getWorktreeBaseDir(): string {
return dirs.rootSubdir("wt", "data");
}
/** Get the path to a worktree directory (~/.omp/wt/<project>/<id>). */
export function getWorktreeDir(encodedProject: string, id: string): string {
return path.join(getWorktreeBaseDir(), encodedProject, id);
}
/** Get the GPU cache path (~/.omp/gpu_cache.json). */
export function getGpuCachePath(): string {
return dirs.rootSubdir("gpu_cache.json", "cache");
}
/** Get the natives directory (~/.omp/natives). */
export function getNativesDir(): string {
return dirs.rootSubdir("natives", "cache");
}
/** Get the stats database path (~/.omp/stats.db). */
export function getStatsDbPath(): string {
return dirs.rootSubdir("stats.db", "data");
}
/** Get the autoresearch state directory (~/.omp/autoresearch). */
export function getAutoresearchDir(): string {
return dirs.rootSubdir("autoresearch", "state");
}
/** Get the per-project autoresearch state directory (~/.omp/autoresearch/<encoded-project>). */
export function getAutoresearchProjectDir(encodedProject: string): string {
return path.join(getAutoresearchDir(), encodedProject);
}
/** Get the per-project autoresearch SQLite database path (~/.omp/autoresearch/<encoded-project>.db). */
export function getAutoresearchDbPath(encodedProject: string): string {
return path.join(getAutoresearchDir(), `${encodedProject}.db`);
}
/** Get the per-run artifact directory (~/.omp/autoresearch/<encoded-project>/runs/<runId>). */
export function getAutoresearchRunDir(encodedProject: string, runId: number): string {
return path.join(getAutoresearchProjectDir(encodedProject), "runs", String(runId).padStart(4, "0"));
}
// =============================================================================
// Agent subdirectories (~/.omp/agent/*)
// =============================================================================
/** Get the path to agent.db (SQLite database for settings and auth storage). */
export function getAgentDbPath(agentDir?: string): string {
return dirs.agentSubdir(agentDir, "agent.db", "data");
}
/** Get the path to history.db (SQLite database for session history). */
export function getHistoryDbPath(agentDir?: string): string {
return dirs.agentSubdir(agentDir, "history.db", "data");
}
/** Get the path to models.db (model cache database). */
export function getModelDbPath(agentDir?: string): string {
return dirs.agentSubdir(agentDir, "models.db", "data");
}
/** Get the sessions directory (~/.omp/agent/sessions). */
export function getSessionsDir(agentDir?: string): string {
return dirs.agentSubdir(agentDir, "sessions", "data");
}
/** Get the content-addressed blob store directory (~/.omp/agent/blobs). */
export function getBlobsDir(agentDir?: string): string {
return dirs.agentSubdir(agentDir, "blobs", "data");
}
/** Get the custom themes directory (~/.omp/agent/themes). */
export function getCustomThemesDir(agentDir?: string): string {
return dirs.agentSubdir(agentDir, "themes");
}
/** Get the tools directory (~/.omp/agent/tools). */
export function getToolsDir(agentDir?: string): string {
return dirs.agentSubdir(agentDir, "tools");
}
/** Get the slash commands directory (~/.omp/agent/commands). */
export function getCommandsDir(agentDir?: string): string {
return dirs.agentSubdir(agentDir, "commands");
}
/** Get the prompts directory (~/.omp/agent/prompts). */
export function getPromptsDir(agentDir?: string): string {
return dirs.agentSubdir(agentDir, "prompts");
}
/** Get the user-level Python modules directory (~/.omp/agent/modules). */
export function getAgentModulesDir(agentDir?: string): string {
return dirs.agentSubdir(agentDir, "modules");
}
/** Get the memories directory (~/.omp/agent/memories). */
export function getMemoriesDir(agentDir?: string): string {
return dirs.agentSubdir(agentDir, "memories", "state");
}
/** Get the terminal sessions directory (~/.omp/agent/terminal-sessions). */
export function getTerminalSessionsDir(agentDir?: string): string {
return dirs.agentSubdir(agentDir, "terminal-sessions", "state");
}
/** Get the crash log path (~/.omp/agent/omp-crash.log). */
export function getCrashLogPath(agentDir?: string): string {
return dirs.agentSubdir(agentDir, "omp-crash.log", "state");
}
/** Get the debug log path (~/.omp/agent/omp-debug.log). */
export function getDebugLogPath(agentDir?: string): string {
return dirs.agentSubdir(agentDir, `${APP_NAME}-debug.log`, "state");
}
// =============================================================================
// Project subdirectories (.omp/*)
// =============================================================================
/** Get the project-level Python modules directory (.omp/modules). */
export function getProjectModulesDir(cwd: string = getProjectDir()): string {
return path.join(getProjectAgentDir(cwd), "modules");
}
/** Get the project-level prompts directory (.omp/prompts). */
export function getProjectPromptsDir(cwd: string = getProjectDir()): string {
return path.join(getProjectAgentDir(cwd), "prompts");
}
/** Get the project-level plugin overrides path (.omp/plugin-overrides.json). */
export function getProjectPluginOverridesPath(cwd: string = getProjectDir()): string {
return path.join(getProjectAgentDir(cwd), "plugin-overrides.json");
}
// =============================================================================
// MCP config paths
// =============================================================================
/** Get the primary MCP config file path (first candidate). */
export function getMCPConfigPath(scope: "user" | "project", cwd: string = getProjectDir()): string {
if (scope === "user") {
return path.join(getAgentDir(), "mcp.json");
}
return path.join(getProjectAgentDir(cwd), "mcp.json");
}
/** Get the SSH config file path. */
export function getSSHConfigPath(scope: "user" | "project", cwd: string = getProjectDir()): string {
if (scope === "user") {
return path.join(getAgentDir(), "ssh.json");
}
return path.join(getProjectAgentDir(cwd), "ssh.json");
}