diff --git a/packages/ai/test/openai-codex-stream.test.ts b/packages/ai/test/openai-codex-stream.test.ts index dcdca7a04..1816498bd 100644 --- a/packages/ai/test/openai-codex-stream.test.ts +++ b/packages/ai/test/openai-codex-stream.test.ts @@ -2,24 +2,21 @@ import { afterEach, describe, expect, it, vi } from "bun:test"; import { streamOpenAICodexResponses } from "@oh-my-pi/pi-ai/providers/openai-codex-responses"; import type { Context, Model } from "@oh-my-pi/pi-ai/types"; import { TempDir } from "@oh-my-pi/pi-utils"; +import { getAgentDir, setAgentDir } from "@oh-my-pi/pi-utils/dirs"; const originalFetch = global.fetch; -const originalAgentDir = Bun.env.PI_CODING_AGENT_DIR; +const originalAgentDir = getAgentDir(); afterEach(() => { global.fetch = originalFetch; - if (originalAgentDir === undefined) { - delete Bun.env.PI_CODING_AGENT_DIR; - } else { - Bun.env.PI_CODING_AGENT_DIR = originalAgentDir; - } + setAgentDir(originalAgentDir); vi.restoreAllMocks(); }); describe("openai-codex streaming", () => { it("streams SSE responses into AssistantMessageEventStream", async () => { const tempDir = TempDir.createSync("@pi-codex-stream-"); - Bun.env.PI_CODING_AGENT_DIR = tempDir.path(); + setAgentDir(tempDir.path()); const payload = Buffer.from( JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }), @@ -130,7 +127,7 @@ describe("openai-codex streaming", () => { it("sets conversation_id/session_id headers and prompt_cache_key when sessionId is provided", async () => { const tempDir = TempDir.createSync("@pi-codex-stream-"); - Bun.env.PI_CODING_AGENT_DIR = tempDir.path(); + setAgentDir(tempDir.path()); const payload = Buffer.from( JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }), @@ -230,7 +227,7 @@ describe("openai-codex streaming", () => { it("clamps gpt-5.3-codex minimal reasoning effort to low", async () => { const tempDir = TempDir.createSync("@pi-codex-stream-"); - Bun.env.PI_CODING_AGENT_DIR = tempDir.path(); + setAgentDir(tempDir.path()); const payload = Buffer.from( JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }), @@ -326,7 +323,7 @@ describe("openai-codex streaming", () => { it("does not set conversation_id/session_id headers when sessionId is not provided", async () => { const tempDir = TempDir.createSync("@pi-codex-stream-"); - Bun.env.PI_CODING_AGENT_DIR = tempDir.path(); + setAgentDir(tempDir.path()); const payload = Buffer.from( JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }), diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 90b8ecb99..1b884c944 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -448,6 +448,7 @@ The `--system-prompt` CLI flag overrides both files. Use `--append-system-prompt ### Custom Models and Providers Add custom models (Ollama, vLLM, LM Studio, etc.) via `~/.omp/agent/models.yml` (`models.json` is still supported for legacy configs): + > See [models.yml provider integration guide](docs/models.md) for full schema, merge behavior, and provider integration patterns. ```json @@ -751,7 +752,7 @@ export default function (omp: HookAPI) { content, display: true, }, - true + true, ); // triggerTurn: start agent loop } }); @@ -1036,33 +1037,6 @@ Omp is a fork of [Pi](https://github.com/badlogic/pi) by [Mario Zechner](https:/ ## Development -### Forking / Rebranding - -Configure via `package.json`: - -```json -{ - "ompConfig": { - "name": "omp", - "configDir": ".omp" - } -} -``` - -Change `name`, `configDir`, and `bin` field for your fork. Affects CLI banner, config paths, and environment variable names. - -### Path Resolution - -Three execution modes: npm install, standalone binary, tsx from source. - -**Always use `src/config.ts`** for package assets: - -```typescript -import { getPackageDir } from "./config.js"; -``` - -Never use `__dirname` directly for package assets. - ### Debug Command `/debug` (hidden) writes rendered lines with ANSI codes to `~/.omp/agent/omp-debug.log` for TUI debugging, as well as the last set of messages that were sent to the LLM. diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 5c05346b3..8437fb986 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -3,10 +3,6 @@ "version": "12.0.0", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", - "ompConfig": { - "name": "omp", - "configDir": ".omp" - }, "bin": { "omp": "src/cli.ts" }, diff --git a/packages/utils/src/dirs.ts b/packages/utils/src/dirs.ts index 5aaedbef5..2f8a2a721 100644 --- a/packages/utils/src/dirs.ts +++ b/packages/utils/src/dirs.ts @@ -6,245 +6,247 @@ */ import * as os from "node:os"; import * as path from "node:path"; -import packageJson from "../../coding-agent/package.json" with { type: "json" }; +import { version } from "../package.json" with { type: "json" }; import { $env } from "./env"; /** App name (e.g. "omp") */ -export const APP_NAME: string = packageJson.ompConfig?.name || "omp"; +export const APP_NAME: string = "omp"; /** Config directory name (e.g. ".omp") */ -export const CONFIG_DIR_NAME: string = packageJson.ompConfig?.configDir || ".omp"; +export const CONFIG_DIR_NAME: string = ".omp"; /** Version (e.g. "1.0.0") */ -export const VERSION: string = packageJson.version; +export const VERSION: string = version; -/** - * Get the config root directory (~/.omp). - */ +// ============================================================================= +// Root directories +// ============================================================================= + +/** Get the config root directory (~/.omp). */ export function getConfigRootDir(): string { return path.join(os.homedir(), $env.PI_CONFIG_DIR || CONFIG_DIR_NAME); } -/** - * Get the path to the agent directory. - */ -export function getAgentDir(): string { - return $env.PI_CODING_AGENT_DIR || path.join(getConfigRootDir(), "agent"); +let agentDir = $env.PI_CODING_AGENT_DIR || path.join(getConfigRootDir(), "agent"); + +/** Set the coding agent directory. */ +export function setAgentDir(dir: string): void { + agentDir = dir; + agentCache.clear(); + $env.PI_CODING_AGENT_DIR = dir; } -/** - * Get the path to the project-local agent directory. - */ +/** Get the agent config directory (~/.omp/agent). */ +export function getAgentDir(): string { + return agentDir; +} + +/** Get the project-local config directory (.omp). */ export function getProjectAgentDir(cwd: string = process.cwd()): string { return path.join(cwd, CONFIG_DIR_NAME); } -/** - * Get the reports directory (~/.omp/reports). - */ -export function getReportsDir(): string { - return path.join(getConfigRootDir(), "reports"); -} +// ============================================================================= +// Caching utilities +// ============================================================================= -/** - * Get the logs directory (~/.omp/logs). - */ -export function getLogsDir(): string { - return path.join(getConfigRootDir(), "logs"); -} +const rootCache = new Map(); -/** - * Get the path to today's log file. - */ -export function getLogPath(date?: string): string { - const today = date ?? new Date().toISOString().slice(0, 10); - return path.join(getLogsDir(), `omp.${today}.log`); -} - -/** - * Get the user-level Python modules directory (~/.omp/agent/modules). - */ -export function getAgentModulesDir(agentDir: string = getAgentDir()): string { - return path.join(agentDir, "modules"); -} - -/** - * Get the project-level Python modules directory (.omp/modules). - */ -export function getProjectModulesDir(cwd: string = process.cwd()): string { - return path.join(getProjectAgentDir(cwd), "modules"); -} - -/** - * Get the project-level prompts directory (.omp/prompts). - */ -export function getProjectPromptsDir(cwd: string = process.cwd()): string { - return path.join(getProjectAgentDir(cwd), "prompts"); -} - -/** - * Get the plugins directory (~/.omp/plugins). - */ -export function getPluginsDir(): string { - return path.join(getConfigRootDir(), "plugins"); -} - -/** 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 path.join(getConfigRootDir(), "remote"); -} - -/** - * Get the SSH control socket directory (~/.omp/ssh-control). - */ -export function getSshControlDir(): string { - return path.join(getConfigRootDir(), "ssh-control"); -} - -/** - * Get the remote host info directory (~/.omp/remote-host). - */ -export function getRemoteHostDir(): string { - return path.join(getConfigRootDir(), "remote-host"); -} - -/** - * Get the managed Python venv directory (~/.omp/python-env). - */ -export function getPythonEnvDir(): string { - return path.join(getConfigRootDir(), "python-env"); -} - -/** - * Get the project-level plugin overrides path (.omp/plugin-overrides.json). - */ -export function getProjectPluginOverridesPath(cwd: string = process.cwd()): string { - return path.join(getProjectAgentDir(cwd), "plugin-overrides.json"); -} - -/** - * Get the MCP config file path. - * @param scope - "user" for ~/.omp/mcp.json or "project" for .omp/mcp.json - */ -export function getMCPConfigPaths(scope: "user" | "project", cwd: string = process.cwd()): string[] { - if (scope === "user") { - return [path.join(getAgentDir(), "mcp.json"), path.join(getAgentDir(), ".mcp.json")]; +function getRootSubdir(subdir: string): string { + if (rootCache.has(subdir)) { + return rootCache.get(subdir); } - return [path.join(getProjectAgentDir(cwd), "mcp.json"), path.join(getProjectAgentDir(cwd), ".mcp.json")]; + const result = path.join(getConfigRootDir(), subdir); + rootCache.set(subdir, result); + return result; } -/** - * Get the primary MCP config file path (first candidate). - * @param scope - "user" for ~/.omp/agent/mcp.json or "project" for .omp/mcp.json - */ -export function getMCPConfigPath(scope: "user" | "project", cwd: string = process.cwd()): string { - return getMCPConfigPaths(scope, cwd)[0]; +const agentCache = new Map(); + +function getAgentSubdir(userAgentDir: string | undefined, subdir: string): string { + if (!userAgentDir || userAgentDir === agentDir) { + if (agentCache.has(subdir)) { + return agentCache.get(subdir); + } else { + const result = path.join(agentDir, subdir); + agentCache.set(subdir, result); + return result; + } + } else { + return path.join(userAgentDir, subdir); + } } -/** - * Get the worktree base directory (~/.omp/wt). - */ +// ============================================================================= +// Config-root subdirectories (~/.omp/*) +// ============================================================================= + +/** Get the reports directory (~/.omp/reports). */ +export function getReportsDir(): string { + return getRootSubdir("reports"); +} + +/** Get the logs directory (~/.omp/logs). */ +export function getLogsDir(): string { + return getRootSubdir("logs"); +} + +/** 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 getRootSubdir("plugins"); +} + +/** Where npm installs packages (~/.omp/plugins/node_modules). */ +export function getPluginsNodeModules(): string { + return getRootSubdir("plugins/node_modules"); +} + +/** Plugin manifest (~/.omp/plugins/package.json). */ +export function getPluginsPackageJson(): string { + return getRootSubdir("plugins/package.json"); +} + +/** Plugin lock file (~/.omp/plugins/omp-plugins.lock.json). */ +export function getPluginsLockfile(): string { + return getRootSubdir("plugins/omp-plugins.lock.json"); +} + +/** Get the remote mount directory (~/.omp/remote). */ +export function getRemoteDir(): string { + return getRootSubdir("remote"); +} + +/** Get the SSH control socket directory (~/.omp/ssh-control). */ +export function getSshControlDir(): string { + return getRootSubdir("ssh-control"); +} + +/** Get the remote host info directory (~/.omp/remote-host). */ +export function getRemoteHostDir(): string { + return getRootSubdir("remote-host"); +} + +/** Get the managed Python venv directory (~/.omp/python-env). */ +export function getPythonEnvDir(): string { + return getRootSubdir("python-env"); +} + +/** Get the worktree base directory (~/.omp/wt). */ export function getWorktreeBaseDir(): string { - return path.join(getConfigRootDir(), "wt"); + return getRootSubdir("wt"); } -/** - * Get the path to a worktree directory. - */ +/** Get the path to a worktree directory (~/.omp/wt//). */ export function getWorktreeDir(encodedProject: string, id: string): string { return path.join(getWorktreeBaseDir(), encodedProject, id); } -/** - * Get the GPU cache path (~/.omp/gpu_cache.json). - */ +/** Get the GPU cache path (~/.omp/gpu_cache.json). */ export function getGpuCachePath(): string { - return path.join(getConfigRootDir(), "gpu_cache.json"); + return getRootSubdir("gpu_cache.json"); } -/** - * Get the test auth database path (~/.omp/agent/testauth.db). - */ -export function getTestAuthPath(): string { - return path.join(getAgentDir(), "testauth.db"); -} - -/** - * Get the sessions directory (~/.omp/agent/sessions). - */ -export function getSessionsDir(): string { - return path.join(getAgentDir(), "sessions"); -} - -/** - * Get the natives directory (~/.omp/natives). - */ +/** Get the natives directory (~/.omp/natives). */ export function getNativesDir(): string { - return path.join(getConfigRootDir(), "natives"); + return getRootSubdir("natives"); } -/** - * Get the stats database path (~/.omp/stats.db). - */ +/** Get the stats database path (~/.omp/stats.db). */ export function getStatsDbPath(): string { - return path.join(getConfigRootDir(), "stats.db"); + return getRootSubdir("stats.db"); } -/** - * Get the crash log path (~/.omp/agent/omp-crash.log). - */ -export function getCrashLogPath(): string { - return path.join(getAgentDir(), "omp-crash.log"); +// ============================================================================= +// Agent subdirectories (~/.omp/agent/*) +// ============================================================================= + +/** Get the path to agent.db (SQLite database for settings and auth storage). */ +export function getAgentDbPath(agentDir?: string): string { + return getAgentSubdir(agentDir, "agent.db"); } -/** Gets the path to agent.db (SQLite database for settings and auth storage) */ -export function getAgentDbPath(agentDir: string = getAgentDir()): string { - return path.join(agentDir, "agent.db"); +/** Get the sessions directory (~/.omp/agent/sessions). */ +export function getSessionsDir(agentDir?: string): string { + return getAgentSubdir(agentDir, "sessions"); } -/** Get path to user's custom themes directory */ -export function getCustomThemesDir(): string { - return path.join(getAgentDir(), "themes"); +/** Get the content-addressed blob store directory (~/.omp/agent/blobs). */ +export function getBlobsDir(agentDir?: string): string { + return getAgentSubdir(agentDir, "blobs"); } -/** Get path to tools directory */ -export function getToolsDir(): string { - return path.join(getAgentDir(), "tools"); +/** Get the custom themes directory (~/.omp/agent/themes). */ +export function getCustomThemesDir(agentDir?: string): string { + return getAgentSubdir(agentDir, "themes"); } -/** Get path to slash commands directory */ -export function getCommandsDir(): string { - return path.join(getAgentDir(), "commands"); +/** Get the tools directory (~/.omp/agent/tools). */ +export function getToolsDir(agentDir?: string): string { + return getAgentSubdir(agentDir, "tools"); } -/** Get path to prompts directory */ -export function getPromptsDir(): string { - return path.join(getAgentDir(), "prompts"); +/** Get the slash commands directory (~/.omp/agent/commands). */ +export function getCommandsDir(agentDir?: string): string { + return getAgentSubdir(agentDir, "commands"); } -/** Get path to content-addressed blob store directory */ -export function getBlobsDir(): string { - return path.join(getAgentDir(), "blobs"); +/** Get the prompts directory (~/.omp/agent/prompts). */ +export function getPromptsDir(agentDir?: string): string { + return getAgentSubdir(agentDir, "prompts"); } -/** Get path to debug log file */ -export function getDebugLogPath(): string { - return path.join(getAgentDir(), `${APP_NAME}-debug.log`); +/** Get the user-level Python modules directory (~/.omp/agent/modules). */ +export function getAgentModulesDir(agentDir?: string): string { + return getAgentSubdir(agentDir, "modules"); +} + +/** Get the test auth database path (~/.omp/agent/testauth.db). */ +export function getTestAuthPath(agentDir?: string): string { + return getAgentSubdir(agentDir, "testauth.db"); +} + +/** Get the crash log path (~/.omp/agent/omp-crash.log). */ +export function getCrashLogPath(agentDir?: string): string { + return getAgentSubdir(agentDir, "omp-crash.log"); +} + +/** Get the debug log path (~/.omp/agent/omp-debug.log). */ +export function getDebugLogPath(agentDir?: string): string { + return getAgentSubdir(agentDir, `${APP_NAME}-debug.log`); +} + +// ============================================================================= +// Project subdirectories (.omp/*) +// ============================================================================= + +/** Get the project-level Python modules directory (.omp/modules). */ +export function getProjectModulesDir(cwd: string = process.cwd()): string { + return path.join(getProjectAgentDir(cwd), "modules"); +} + +/** Get the project-level prompts directory (.omp/prompts). */ +export function getProjectPromptsDir(cwd: string = process.cwd()): string { + return path.join(getProjectAgentDir(cwd), "prompts"); +} + +/** Get the project-level plugin overrides path (.omp/plugin-overrides.json). */ +export function getProjectPluginOverridesPath(cwd: string = process.cwd()): 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 = process.cwd()): string { + if (scope === "user") { + return path.join(getAgentDir(), "mcp.json"); + } + return path.join(getProjectAgentDir(cwd), "mcp.json"); }