- 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.
446 lines
16 KiB
TypeScript
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");
|
|
}
|