refactor(dirs): centralized directory path utilities into @oh-my-pi/pi-utils/dirs module

- Extracted directory path utilities from multiple packages into a centralized '@oh-my-pi/pi-utils/dirs' module.
- Moved 30+ path helper functions (getAgentDir, getConfigRootDir, getPluginsDir, getMCPConfigPath, etc.) from scattered locations into a single shared utility module.
- Consolidated APP_NAME, CONFIG_DIR_NAME, and VERSION constants into the centralized dirs module for reuse across packages.
- Updated 70+ import statements across packages/ai, packages/coding-agent, packages/stats, and packages/tui to use the new centralized module.
- Removed local path construction logic and replaced with utility function calls for improved maintainability and consistency.
- Deleted packages/coding-agent/src/extensibility/plugins/paths.ts as its functions were moved to the centralized dirs module.
This commit is contained in:
can1357
2026-02-13 15:06:05 +01:00
parent 402713a1ee
commit fcd7ff4f84
73 changed files with 456 additions and 347 deletions
+1 -10
View File
@@ -5,9 +5,8 @@
import { Database, type Statement } from "bun:sqlite";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { $env } from "@oh-my-pi/pi-utils";
import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
import type { OAuthCredentials } from "./utils/oauth/types";
type AuthCredential = { type: "api_key"; key: string } | ({ type: "oauth" } & OAuthCredentials);
@@ -21,14 +20,6 @@ type AuthRow = {
updated_at: number;
};
/**
* Get the agent config directory (e.g., ~/.omp/agent/)
*/
function getAgentDir(): string {
const configDir = $env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".omp", "agent");
return configDir;
}
/**
* Get path to agent.db
*/
+1 -5
View File
@@ -7,6 +7,7 @@ import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { $env, abortableSleep, isEnoent } from "@oh-my-pi/pi-utils";
import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
import packageJson from "../../../package.json" with { type: "json" };
import type { OAuthController, OAuthCredentials } from "./types";
@@ -36,11 +37,6 @@ interface TokenResponse {
interval?: number;
}
function getAgentDir(): string {
const configDir = $env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".omp", "agent");
return configDir;
}
function resolveOAuthHost(): string {
return $env.KIMI_CODE_OAUTH_HOST || $env.KIMI_OAUTH_HOST || DEFAULT_OAUTH_HOST;
}
+2 -3
View File
@@ -7,11 +7,10 @@
* E2E tests are disabled by default. Set E2E=1 environment variable to enable.
*/
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { getOAuthApiKey } from "@oh-my-pi/pi-ai/utils/oauth";
import type { OAuthCredentials, OAuthProvider } from "@oh-my-pi/pi-ai/utils/oauth/types";
import { isEnoent } from "@oh-my-pi/pi-utils";
import { getTestAuthPath } from "@oh-my-pi/pi-utils/dirs";
/**
* E2E tests require explicit opt-in via E2E=1 environment variable.
@@ -28,7 +27,7 @@ export function e2eApiKey(envVar: string): string | undefined {
return Bun.env[envVar];
}
const AUTH_PATH = path.join(os.homedir(), ".omp", "agent", "testauth.db");
const AUTH_PATH = getTestAuthPath();
type ApiKeyCredential = {
type: "api_key";
+2 -1
View File
@@ -1,7 +1,6 @@
# Changelog
## [Unreleased]
### Added
- Comprehensive `models.yml` provider integration guide documenting custom model registration, provider overrides, API adapters, merge behavior, and practical integration patterns for Ollama, vLLM, LM Studio, and proxy endpoints
@@ -9,6 +8,8 @@
### Changed
- Moved directory path utilities from `src/config.ts` to `@oh-my-pi/pi-utils/dirs` for shared use across packages
- Updated imports throughout codebase to use centralized directory path functions from `@oh-my-pi/pi-utils/dirs`
- Updated interactive bash terminal UI label from 'InteractiveTerm' to 'Console' for clarity
- Enhanced bash execution environment with comprehensive non-interactive defaults for pagers, editors, and package managers to prevent command blocking and interactive prompts
- Updated custom models configuration to use `~/.omp/agent/models.yml` (YAML format) while maintaining backward compatibility with legacy `models.json`
+1 -1
View File
@@ -4,7 +4,7 @@
* lightweight CLI runner from pi-utils.
*/
import { type CommandEntry, run } from "@oh-my-pi/pi-utils/cli";
import { APP_NAME, VERSION } from "./config";
import { APP_NAME, VERSION } from "@oh-my-pi/pi-utils/dirs";
// Detect known Bun errata that cause TUI crashes (e.g. Bun.stringWidth mishandling OSC sequences).
if (Bun.stringWidth("\x1b[0m\x1b]8;;\x07") !== 0) {
+1 -1
View File
@@ -3,8 +3,8 @@
*/
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { logger } from "@oh-my-pi/pi-utils";
import { APP_NAME, CONFIG_DIR_NAME } from "@oh-my-pi/pi-utils/dirs";
import chalk from "chalk";
import { APP_NAME, CONFIG_DIR_NAME } from "../config";
import { BUILTIN_TOOLS } from "../tools";
export type Mode = "text" | "json" | "rpc";
+2 -1
View File
@@ -4,8 +4,9 @@
* Handles `omp config <command>` subcommands for managing settings.
* Uses settings-defs as the source of truth for available settings.
*/
import { APP_NAME, getAgentDir } from "@oh-my-pi/pi-utils/dirs";
import chalk from "chalk";
import { APP_NAME, getAgentDir } from "../config";
import {
getDefault,
getEnumValues,
+1 -1
View File
@@ -5,8 +5,8 @@
*/
import * as path from "node:path";
import { grep } from "@oh-my-pi/pi-natives";
import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
import chalk from "chalk";
import { APP_NAME } from "../config";
export interface GrepCommandArgs {
pattern: string;
+2 -1
View File
@@ -3,8 +3,9 @@
*
* Handles `omp jupyter` subcommand for managing the shared Python gateway.
*/
import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
import chalk from "chalk";
import { APP_NAME } from "../config";
import { getGatewayStatus, shutdownSharedGateway } from "../ipy/gateway-coordinator";
export type JupyterAction = "kill" | "status";
+2 -1
View File
@@ -3,8 +3,9 @@
*
* Handles `omp plugin <command>` subcommands for plugin lifecycle management.
*/
import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
import chalk from "chalk";
import { APP_NAME } from "../config";
import { PluginManager, parseSettingValue, validateSetting } from "../extensibility/plugins";
import { theme } from "../modes/theme/theme";
+2 -3
View File
@@ -3,11 +3,10 @@
*
* Handles `omp setup <component>` to install dependencies for optional features.
*/
import * as os from "node:os";
import * as path from "node:path";
import { APP_NAME, getPythonEnvDir } from "@oh-my-pi/pi-utils/dirs";
import { $ } from "bun";
import chalk from "chalk";
import { APP_NAME, CONFIG_DIR_NAME } from "../config";
import { theme } from "../modes/theme/theme";
export type SetupComponent = "python";
@@ -23,7 +22,7 @@ export interface SetupCommandArgs {
const VALID_COMPONENTS: SetupComponent[] = ["python"];
const PYTHON_PACKAGES = ["jupyter_kernel_gateway", "ipykernel"];
const MANAGED_PYTHON_ENV = path.join(os.homedir(), CONFIG_DIR_NAME, "python-env");
const MANAGED_PYTHON_ENV = getPythonEnvDir();
/**
* Parse setup subcommand arguments.
+1 -1
View File
@@ -6,8 +6,8 @@
import * as path from "node:path";
import { createInterface } from "node:readline/promises";
import { Shell } from "@oh-my-pi/pi-natives";
import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
import chalk from "chalk";
import { APP_NAME } from "../config";
import { Settings } from "../config/settings";
import { getOrCreateSnapshot } from "../utils/shell-snapshot";
+2 -1
View File
@@ -3,8 +3,9 @@
*
* Handles `omp stats` subcommand for viewing AI usage statistics.
*/
import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
import chalk from "chalk";
import { APP_NAME } from "../config";
import { openPath } from "../utils/open";
// =============================================================================
+1 -1
View File
@@ -9,8 +9,8 @@ import * as fs from "node:fs";
import * as path from "node:path";
import { pipeline } from "node:stream/promises";
import { isEnoent } from "@oh-my-pi/pi-utils";
import { APP_NAME, VERSION } from "@oh-my-pi/pi-utils/dirs";
import chalk from "chalk";
import { APP_NAME, VERSION } from "../config";
import { theme } from "../modes/theme/theme";
/**
@@ -3,8 +3,9 @@
*
* Handles `omp q`/`omp web-search` subcommands for testing web search providers.
*/
import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
import chalk from "chalk";
import { APP_NAME } from "../config";
import { initTheme, theme } from "../modes/theme/theme";
import { runSearchQuery, type SearchParams } from "../web/search/index";
import { renderSearchResult } from "../web/search/render";
+1 -1
View File
@@ -2,8 +2,8 @@
* Root command for the coding agent CLI.
*/
import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
import { parseArgs } from "../cli/args";
import { APP_NAME } from "../config";
import { runRootCommand } from "../main";
export default class Index extends Command {
+3 -62
View File
@@ -1,25 +1,15 @@
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { $env, isEnoent, logger } from "@oh-my-pi/pi-utils";
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
import { CONFIG_DIR_NAME, getAgentDir } from "@oh-my-pi/pi-utils/dirs";
import type { TSchema } from "@sinclair/typebox";
import { Value } from "@sinclair/typebox/value";
import { Ajv, type ErrorObject, type ValidateFunction } from "ajv";
import { JSONC, YAML } from "bun";
// Embed package.json at build time for config
import packageJson from "../package.json" with { type: "json" };
// =============================================================================
// App Config (from embedded package.json)
// =============================================================================
export const APP_NAME: string = (packageJson as { ompConfig?: { name?: string } }).ompConfig?.name || "omp";
export const CONFIG_DIR_NAME: string =
(packageJson as { ompConfig?: { configDir?: string } }).ompConfig?.configDir || ".omp";
export const VERSION: string = (packageJson as { version: string }).version;
const priorityList = [
{ dir: ".omp", globalAgentDir: ".omp/agent" },
{ dir: CONFIG_DIR_NAME, globalAgentDir: `${CONFIG_DIR_NAME}/agent` },
{ dir: ".claude" },
{ dir: ".codex" },
{ dir: ".gemini" },
@@ -253,55 +243,6 @@ export class ConfigFile<T> implements IConfigFile<T> {
}
}
/** Get the agent config directory (e.g., ~/.omp/agent/) */
export function getAgentDir(): string {
return $env.PI_CODING_AGENT_DIR || path.join(os.homedir(), CONFIG_DIR_NAME, "agent");
}
/** Get path to user's custom themes directory */
export function getCustomThemesDir(): string {
return path.join(getAgentDir(), "themes");
}
/**
* Gets the path to agent.db (SQLite database for settings and auth storage).
* @param agentDir - Base agent directory, defaults to ~/.omp/agent
* @returns Absolute path to the agent.db file
*/
export function getAgentDbPath(agentDir: string = getAgentDir()): string {
return path.join(agentDir, "agent.db");
}
/** Get path to tools directory */
export function getToolsDir(): string {
return path.join(getAgentDir(), "tools");
}
/** Get path to slash commands directory */
export function getCommandsDir(): string {
return path.join(getAgentDir(), "commands");
}
/** Get path to prompts directory */
export function getPromptsDir(): string {
return path.join(getAgentDir(), "prompts");
}
/** Get path to content-addressed blob store directory */
export function getBlobsDir(): string {
return path.join(getAgentDir(), "blobs");
}
/** Get path to sessions directory */
export function getSessionsDir(): string {
return path.join(getAgentDir(), "sessions");
}
/** Get path to debug log file */
export function getDebugLogPath(): string {
return path.join(getAgentDir(), `${APP_NAME}-debug.log`);
}
// =============================================================================
// Multi-Config Directory Helpers
// =============================================================================
@@ -9,7 +9,7 @@ import {
setEditorKeybindings,
} from "@oh-my-pi/pi-tui";
import { logger } from "@oh-my-pi/pi-utils";
import { getAgentDir } from "../config";
import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
/**
* Application-level actions (coding agent specific).
@@ -1,8 +1,8 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { logger } from "@oh-my-pi/pi-utils";
import { getProjectPromptsDir, getPromptsDir } from "@oh-my-pi/pi-utils/dirs";
import Handlebars from "handlebars";
import { CONFIG_DIR_NAME, getPromptsDir } from "../config";
import { computeLineHash } from "../patch/hashline";
import { jtdToTypeScript } from "../tools/jtd-to-typescript";
import { parseFrontmatter } from "../utils/frontmatter";
@@ -468,7 +468,7 @@ export interface LoadPromptTemplatesOptions {
/**
* Load all prompt templates from:
* 1. Global: agentDir/prompts/
* 2. Project: cwd/{CONFIG_DIR_NAME}/prompts/
* 2. Project: cwd/.omp/prompts/
*/
export async function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): Promise<PromptTemplate[]> {
const resolvedCwd = options.cwd ?? process.cwd();
@@ -481,8 +481,8 @@ export async function loadPromptTemplates(options: LoadPromptTemplatesOptions =
const globalPromptsDir = options.agentDir ? path.join(options.agentDir, "prompts") : resolvedAgentDir;
templates.push(...(await loadTemplatesFromDir(globalPromptsDir, "user")));
// 2. Load project templates from cwd/{CONFIG_DIR_NAME}/prompts/
const projectPromptsDir = path.resolve(resolvedCwd, CONFIG_DIR_NAME, "prompts");
// 2. Load project templates from cwd/.omp/prompts/
const projectPromptsDir = getProjectPromptsDir(resolvedCwd);
templates.push(...(await loadTemplatesFromDir(projectPromptsDir, "project")));
return templates;
+1 -1
View File
@@ -14,9 +14,9 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
import { getAgentDbPath, getAgentDir } from "@oh-my-pi/pi-utils/dirs";
import { YAML } from "bun";
import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings";
import { getAgentDbPath, getAgentDir } from "../config";
import type { ModelRole } from "../config/model-registry";
import { loadCapability } from "../discovery";
import { setColorBlindMode, setSymbolPreset, setTheme } from "../modes/theme/theme";
+1 -1
View File
@@ -6,7 +6,7 @@
import * as fs from "node:fs/promises";
import { getWorkProfile } from "@oh-my-pi/pi-natives";
import { Container, Loader, type SelectItem, SelectList, Spacer, Text } from "@oh-my-pi/pi-tui";
import { getSessionsDir } from "../config";
import { getSessionsDir } from "@oh-my-pi/pi-utils/dirs";
import { DynamicBorder } from "../modes/components/dynamic-border";
import { getSelectListTheme, getSymbolTheme, theme } from "../modes/theme/theme";
import type { InteractiveModeContext } from "../modes/types";
@@ -4,24 +4,13 @@
* Creates a .tar.gz archive with session data, logs, system info, and optional profiling data.
*/
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import type { WorkProfile } from "@oh-my-pi/pi-natives";
import { isEnoent } from "@oh-my-pi/pi-utils";
import { getLogPath, getReportsDir } from "@oh-my-pi/pi-utils/dirs";
import type { CpuProfile, HeapSnapshot } from "./profiler";
import { collectSystemInfo, sanitizeEnv } from "./system-info";
/** Reports directory path */
export function getReportsDir(): string {
return path.join(os.homedir(), ".omp", "reports");
}
/** Get today's log file path */
function getLogPath(): string {
const today = new Date().toISOString().slice(0, 10);
return path.join(os.homedir(), ".omp", "logs", `omp.${today}.log`);
}
/** Read last N lines from a file */
async function readLastLines(filePath: string, n: number): Promise<string> {
try {
@@ -2,7 +2,7 @@
* System information collection for debug reports.
*/
import * as os from "node:os";
import { VERSION } from "../config";
import { VERSION } from "@oh-my-pi/pi-utils/dirs";
export interface SystemInfo {
os: string;
@@ -4,6 +4,7 @@
import * as os from "node:os";
import * as path from "node:path";
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { CONFIG_DIR_NAME } from "@oh-my-pi/pi-utils/dirs";
import { readDirEntries, readFile } from "../capability/fs";
import type { Skill, SkillFrontmatter } from "../capability/skill";
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
@@ -39,9 +40,9 @@ export function expandPath(p: string): string {
*/
export const SOURCE_PATHS = {
native: {
userBase: ".omp",
userAgent: ".omp/agent",
projectDir: ".omp",
userBase: CONFIG_DIR_NAME,
userAgent: `${CONFIG_DIR_NAME}/agent`,
projectDir: CONFIG_DIR_NAME,
},
claude: {
userBase: ".claude",
@@ -6,7 +6,7 @@
*/
import * as fs from "node:fs";
import * as path from "node:path";
import { getAgentDir } from "../config";
import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
export interface CustomShareResult {
/** URL to display/open (optional - script may handle everything itself) */
@@ -1,7 +1,7 @@
import * as path from "node:path";
import type { AgentState } from "@oh-my-pi/pi-agent-core";
import { isEnoent } from "@oh-my-pi/pi-utils";
import { APP_NAME } from "../../config";
import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
import { getResolvedThemeColors, getThemeExportColors } from "../../modes/theme/theme";
import { type SessionEntry, type SessionHeader, SessionManager } from "../../session/session-manager";
// Pre-generated template (created by scripts/generate-template.ts at publish time)
@@ -8,8 +8,9 @@ import * as fs from "node:fs";
import * as path from "node:path";
import * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
import * as typebox from "@sinclair/typebox";
import { getAgentDir, getConfigDirs } from "../../config";
import { getConfigDirs } from "../../config";
import { execCommand } from "../../exec/exec";
import { ReviewCommand } from "./bundled/review";
import type {
@@ -14,13 +14,6 @@ export {
} from "./loader";
export { PluginManager, parseSettingValue, validateSetting } from "./manager";
export { extractPackageName, formatPluginSpec, parsePluginSpec } from "./parser";
export {
getPluginsDir,
getPluginsLockfile,
getPluginsNodeModules,
getPluginsPackageJson,
getProjectPluginOverrides,
} from "./paths";
export type {
BooleanSetting,
DoctorCheck,
@@ -1,7 +1,7 @@
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { isEnoent } from "@oh-my-pi/pi-utils";
import { getAgentDir } from "../../config";
import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
import type { InstalledPlugin } from "./types";
const PLUGINS_DIR = path.join(getAgentDir(), "plugins");
@@ -7,12 +7,8 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { isEnoent } from "@oh-my-pi/pi-utils";
import {
getAllProjectPluginOverridePaths,
getPluginsLockfile,
getPluginsNodeModules,
getPluginsPackageJson,
} from "./paths";
import { getPluginsLockfile, getPluginsNodeModules, getPluginsPackageJson } from "@oh-my-pi/pi-utils/dirs";
import { getConfigDirPaths } from "../../config";
import type { InstalledPlugin, PluginManifest, PluginRuntimeConfig, ProjectPluginOverrides } from "./types";
// =============================================================================
@@ -36,7 +32,7 @@ async function loadRuntimeConfig(): Promise<PluginRuntimeConfig> {
* Load project-local plugin overrides (checks .omp and .pi directories).
*/
async function loadProjectOverrides(cwd: string): Promise<ProjectPluginOverrides> {
for (const overridesPath of getAllProjectPluginOverridePaths(cwd)) {
for (const overridesPath of getConfigDirPaths("plugin-overrides.json", { user: false, cwd })) {
try {
return await Bun.file(overridesPath).json();
} catch (err) {
@@ -1,14 +1,14 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
import { extractPackageName, parsePluginSpec } from "./parser";
import {
getPluginsDir,
getPluginsLockfile,
getPluginsNodeModules,
getPluginsPackageJson,
getProjectPluginOverrides,
} from "./paths";
getProjectPluginOverridesPath,
} from "@oh-my-pi/pi-utils/dirs";
import { extractPackageName, parsePluginSpec } from "./parser";
import type {
DoctorCheck,
DoctorOptions,
@@ -82,7 +82,7 @@ export class PluginManager {
}
async #loadProjectOverrides(): Promise<ProjectPluginOverrides> {
const overridesPath = getProjectPluginOverrides(this.#cwd);
const overridesPath = getProjectPluginOverridesPath(this.#cwd);
try {
return await Bun.file(overridesPath).json();
} catch (err) {
@@ -1,37 +0,0 @@
import * as os from "node:os";
import * as path from "node:path";
import { CONFIG_DIR_NAME, getConfigDirPaths } from "../../config";
// =============================================================================
// Plugin Directory Paths
// =============================================================================
/** Root plugin directory: ~/.omp/plugins (not under agent/) */
export function getPluginsDir(): string {
return path.join(os.homedir(), CONFIG_DIR_NAME, "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");
}
/** Project-local plugin overrides: .omp/plugin-overrides.json (primary) */
export function getProjectPluginOverrides(cwd: string): string {
return path.join(cwd, CONFIG_DIR_NAME, "plugin-overrides.json");
}
/** All possible project plugin override paths (primary + legacy) */
export function getAllProjectPluginOverridePaths(cwd: string): string[] {
return getConfigDirPaths("plugin-overrides.json", { user: false, cwd });
}
+1 -1
View File
@@ -7,7 +7,7 @@ export { StringEnum } from "@oh-my-pi/pi-ai";
export { Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
// Logging
export { logger } from "@oh-my-pi/pi-utils";
export { getAgentDir, VERSION } from "./config";
export { getAgentDir, VERSION } from "@oh-my-pi/pi-utils/dirs";
export { formatKeyHint, formatKeyHints } from "./config/keybindings";
export { ModelRegistry } from "./config/model-registry";
// Prompt templates
+1 -1
View File
@@ -1,6 +1,6 @@
import * as path from "node:path";
import { $env, isEnoent, logger } from "@oh-my-pi/pi-utils";
import { getAgentDir } from "../config";
import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
import { OutputSink } from "../session/streaming-output";
import { time } from "../utils/timings";
import { shutdownSharedGateway } from "./gateway-coordinator";
@@ -2,8 +2,8 @@ import * as fs from "node:fs";
import { createServer } from "node:net";
import * as path from "node:path";
import { isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
import type { Subprocess } from "bun";
import { getAgentDir } from "../config";
import { Settings } from "../config/settings";
import { getOrCreateSnapshot } from "../utils/shell-snapshot";
import { time } from "../utils/timings";
+5 -6
View File
@@ -1,6 +1,6 @@
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { getAgentModulesDir, getProjectModulesDir } from "@oh-my-pi/pi-utils/dirs";
export type PythonModuleSource = "user" | "project";
@@ -26,8 +26,8 @@ export interface PythonModuleExecutor {
export interface DiscoverPythonModulesOptions {
/** Working directory for project-level modules. Default: process.cwd() */
cwd?: string;
/** Home directory for user-level modules. Default: os.homedir() */
homeDir?: string;
/** Agent directory for user-level modules. Default: from getAgentDir() */
agentDir?: string;
}
interface ModuleCandidate {
@@ -66,10 +66,9 @@ async function readModuleContent(candidate: ModuleCandidate): Promise<PythonModu
*/
export async function discoverPythonModules(options: DiscoverPythonModulesOptions = {}): Promise<PythonModuleEntry[]> {
const cwd = options.cwd ?? process.cwd();
const homeDir = options.homeDir ?? os.homedir();
const userDir = path.join(homeDir, ".omp", "agent", "modules");
const projectDir = path.resolve(cwd, ".omp", "modules");
const userDir = getAgentModulesDir(options.agentDir);
const projectDir = getProjectModulesDir(cwd);
const userCandidates = await listModuleCandidates(userDir, "user");
const projectCandidates = await listModuleCandidates(projectDir, "project");
+2 -3
View File
@@ -5,11 +5,10 @@
* for both the shared gateway and local kernel spawning.
*/
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { $env } from "@oh-my-pi/pi-utils";
import { CONFIG_DIR_NAME } from "../config";
import { getPythonEnvDir } from "@oh-my-pi/pi-utils/dirs";
const DEFAULT_ENV_ALLOWLIST = new Set([
"PATH",
@@ -106,7 +105,7 @@ function resolvePathKey(env: Record<string, string | undefined>): string {
}
function resolveManagedPythonEnv(): string {
return path.join(os.homedir(), CONFIG_DIR_NAME, "python-env");
return getPythonEnvDir();
}
function resolveManagedPythonCandidate(): { venvPath: string; pythonPath: string } {
+2 -1
View File
@@ -10,12 +10,13 @@ import * as path from "node:path";
import { createInterface } from "node:readline/promises";
import { type ImageContent, supportsXhigh } from "@oh-my-pi/pi-ai";
import { $env, postmortem } from "@oh-my-pi/pi-utils";
import { VERSION } from "@oh-my-pi/pi-utils/dirs";
import chalk from "chalk";
import type { Args } from "./cli/args";
import { processFileArguments } from "./cli/file-processor";
import { listModels } from "./cli/list-models";
import { selectSession } from "./cli/session-picker";
import { findConfigFile, VERSION } from "./config";
import { findConfigFile } from "./config";
import { ModelRegistry, ModelsConfigFile } from "./config/model-registry";
import { parseModelPattern, parseModelString, resolveModelScope, type ScopedModel } from "./config/model-resolver";
import { Settings, settings } from "./config/settings";
+1 -13
View File
@@ -4,24 +4,12 @@
* Utilities for reading/writing .omp/mcp.json files at user or project level.
*/
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { isEnoent } from "@oh-my-pi/pi-utils";
import { validateServerConfig } from "./config";
import type { MCPConfigFile, MCPServerConfig } from "./types";
/**
* Get the path to the MCP config file.
* @param scope - "user" for ~/.omp/mcp.json or "project" for .omp/mcp.json
* @param cwd - Current working directory (used for project scope)
*/
export function getMCPConfigPath(scope: "user" | "project", cwd: string): string {
if (scope === "user") {
return path.join(os.homedir(), ".omp", "mcp.json");
}
return path.join(cwd, ".omp", "mcp.json");
}
/**
* Read an MCP config file.
* Returns empty config if file doesn't exist.
@@ -1,5 +1,5 @@
import { type Component, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
import { APP_NAME } from "../../config";
import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
import { theme } from "../../modes/theme/theme";
export interface RecentSession {
@@ -4,16 +4,11 @@
* Handles /mcp subcommands for managing MCP servers.
*/
import { Spacer, Text } from "@oh-my-pi/pi-tui";
import { getMCPConfigPath } from "@oh-my-pi/pi-utils/dirs";
import type { SourceMeta } from "../../capability/types";
import { analyzeAuthError, discoverOAuthEndpoints, MCPManager } from "../../mcp";
import { connectToServer, disconnectServer, listTools } from "../../mcp/client";
import {
addMCPServer,
getMCPConfigPath,
readMCPConfigFile,
removeMCPServer,
updateMCPServer,
} from "../../mcp/config-writer";
import { addMCPServer, readMCPConfigFile, removeMCPServer, updateMCPServer } from "../../mcp/config-writer";
import { MCPOAuthFlow } from "../../mcp/oauth-flow";
import type { MCPServerConfig, MCPServerConnection } from "../../mcp/types";
import type { OAuthCredential } from "../../session/auth-storage";
@@ -2,7 +2,7 @@ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { OAuthProvider } from "@oh-my-pi/pi-ai";
import type { Component } from "@oh-my-pi/pi-tui";
import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
import { getAgentDbPath } from "../../config";
import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs";
import { MODEL_ROLES } from "../../config/model-registry";
import { settings } from "../../config/settings";
import { DebugSelectorComponent } from "../../debug";
@@ -16,8 +16,8 @@ import {
TUI,
} from "@oh-my-pi/pi-tui";
import { $env, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
import chalk from "chalk";
import { APP_NAME } from "../config";
import { KeybindingsManager } from "../config/keybindings";
import { renderPromptTemplate } from "../config/prompt-templates";
import { type Settings, settings } from "../config/settings";
@@ -7,10 +7,10 @@ import {
} from "@oh-my-pi/pi-natives";
import type { EditorTheme, MarkdownTheme, SelectListTheme, SymbolTheme } from "@oh-my-pi/pi-tui";
import { adjustHsv, isEnoent, logger } from "@oh-my-pi/pi-utils";
import { getCustomThemesDir } from "@oh-my-pi/pi-utils/dirs";
import { type Static, Type } from "@sinclair/typebox";
import { TypeCompiler } from "@sinclair/typebox/compiler";
import chalk from "chalk";
import { getCustomThemesDir } from "../../config";
// Embed theme JSON files at build time
import darkThemeJson from "./dark.json" with { type: "json" };
import { defaultThemes } from "./defaults";
+14 -5
View File
@@ -11,7 +11,6 @@
import * as fs from "node:fs/promises";
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import { StringEnum } from "@oh-my-pi/pi-ai";
import { invalidateFsScanCache } from "@oh-my-pi/pi-natives";
import { type Static, Type } from "@sinclair/typebox";
import { renderPromptTemplate } from "../config/prompt-templates";
import {
@@ -25,6 +24,11 @@ import hashlineDescription from "../prompts/tools/hashline.md" with { type: "tex
import patchDescription from "../prompts/tools/patch.md" with { type: "text" };
import replaceDescription from "../prompts/tools/replace.md" with { type: "text" };
import type { ToolSession } from "../tools";
import {
invalidateFsScanAfterDelete,
invalidateFsScanAfterRename,
invalidateFsScanAfterWrite,
} from "../tools/fs-cache-invalidation";
import { outputMeta } from "../tools/output-meta";
import { enforcePlanModeWrite, resolvePlanPath } from "../tools/plan-mode-guard";
import { applyPatch } from "./applicator";
@@ -531,7 +535,7 @@ export class EditTool implements AgentTool<TInput> {
const finalContent = bom + restoreLineEndings(result.content, originalEnding);
const diagnostics = await this.#writethrough(absolutePath, finalContent, signal, file, batchRequest);
invalidateFsScanCache(absolutePath);
invalidateFsScanAfterWrite(absolutePath);
const diffResult = generateDiffString(originalNormalized, result.content);
const normative = buildNormativeUpdateInput({
@@ -589,8 +593,13 @@ export class EditTool implements AgentTool<TInput> {
fuzzyThreshold: this.#fuzzyThreshold,
allowFuzzy: this.#allowFuzzy,
});
invalidateFsScanCache(resolvedPath);
if (resolvedRename) invalidateFsScanCache(resolvedRename);
if (resolvedRename) {
invalidateFsScanAfterRename(resolvedPath, resolvedRename);
} else if (result.change.type === "delete") {
invalidateFsScanAfterDelete(resolvedPath);
} else {
invalidateFsScanAfterWrite(resolvedPath);
}
const effRename = result.change.newPath ? rename : undefined;
// Generate diff for display
@@ -713,7 +722,7 @@ export class EditTool implements AgentTool<TInput> {
const finalContent = bom + restoreLineEndings(result.content, originalEnding);
const diagnostics = await this.#writethrough(absolutePath, finalContent, signal, file, batchRequest);
invalidateFsScanCache(absolutePath);
invalidateFsScanAfterWrite(absolutePath);
const diffResult = generateDiffString(normalizedContent, result.content);
const resultText =
+1 -1
View File
@@ -2,10 +2,10 @@ import { Agent, type AgentEvent, type AgentMessage, type AgentTool, type Thinkin
import { type Message, type Model, supportsXhigh } from "@oh-my-pi/pi-ai";
import type { Component } from "@oh-my-pi/pi-tui";
import { $env, logger, postmortem } from "@oh-my-pi/pi-utils";
import { getAgentDbPath, getAgentDir } from "@oh-my-pi/pi-utils/dirs";
import chalk from "chalk";
import { loadCapability } from "./capability";
import { type Rule, ruleCapability } from "./capability/rule";
import { getAgentDbPath, getAgentDir } from "./config";
import { ModelRegistry } from "./config/model-registry";
import { formatModelString, parseModelString } from "./config/model-resolver";
import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate } from "./config/prompt-templates";
@@ -29,8 +29,8 @@ import type {
} from "@oh-my-pi/pi-ai";
import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@oh-my-pi/pi-ai";
import { abortableSleep, isEnoent, logger } from "@oh-my-pi/pi-utils";
import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs";
import type { Rule } from "../capability/rule";
import { getAgentDbPath } from "../config";
import { MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "../config/model-registry";
import { expandRoleAlias, parseModelString } from "../config/model-resolver";
import {
@@ -2,7 +2,7 @@ import { Database, type Statement } from "bun:sqlite";
import * as fs from "node:fs";
import * as path from "node:path";
import { logger } from "@oh-my-pi/pi-utils";
import { getAgentDbPath } from "../config";
import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs";
import type { RawSettings as Settings } from "../config/settings";
import type { AuthCredential } from "./auth-storage";
@@ -2,7 +2,7 @@ import { Database, type Statement } from "bun:sqlite";
import * as fs from "node:fs";
import * as path from "node:path";
import { logger } from "@oh-my-pi/pi-utils";
import { getAgentDir } from "../config";
import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
export interface HistoryEntry {
id: number;
@@ -3,7 +3,7 @@ import * as path from "node:path";
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
import type { ImageContent, Message, TextContent, Usage } from "@oh-my-pi/pi-ai";
import { isEnoent, logger, parseJsonlLenient, Snowflake } from "@oh-my-pi/pi-utils";
import { getBlobsDir, getAgentDir as getDefaultAgentDir } from "../config";
import { getBlobsDir, getAgentDir as getDefaultAgentDir } from "@oh-my-pi/pi-utils/dirs";
import { type BlobPutResult, BlobStore, externalizeImageData, isBlobRef, resolveImageData } from "./blob-store";
import {
type BashExecutionMessage,
@@ -1,9 +1,8 @@
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
import { getRemoteHostDir, getSshControlDir } from "@oh-my-pi/pi-utils/dirs";
import { $ } from "bun";
import { CONFIG_DIR_NAME } from "../config";
export interface SSHConnectionTarget {
name: string;
@@ -25,9 +24,9 @@ export interface SSHHostInfo {
compatEnabled: boolean;
}
const CONTROL_DIR = path.join(os.homedir(), CONFIG_DIR_NAME, "ssh-control");
const CONTROL_DIR = getSshControlDir();
const CONTROL_PATH = path.join(CONTROL_DIR, "%h.sock");
const HOST_INFO_DIR = path.join(os.homedir(), CONFIG_DIR_NAME, "remote-host");
const HOST_INFO_DIR = getRemoteHostDir();
const HOST_INFO_VERSION = 2;
const activeHosts = new Map<string, SSHConnectionTarget>();
+2 -3
View File
@@ -1,11 +1,10 @@
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { getRemoteDir } from "@oh-my-pi/pi-utils/dirs";
import { $ } from "bun";
import { CONFIG_DIR_NAME } from "../config";
import { getControlDir, getControlPathTemplate, type SSHConnectionTarget } from "./connection-manager";
const REMOTE_DIR = path.join(os.homedir(), CONFIG_DIR_NAME, "remote");
const REMOTE_DIR = getRemoteDir();
const CONTROL_DIR = getControlDir();
const CONTROL_PATH = getControlPathTemplate();
+2 -2
View File
@@ -2,9 +2,9 @@
* System prompt construction and project context loading
*/
import * as os from "node:os";
import * as path from "node:path";
import { getSystemInfo as getNativeSystemInfo, type SystemInfo } from "@oh-my-pi/pi-natives";
import { $env, logger } from "@oh-my-pi/pi-utils";
import { getGpuCachePath } from "@oh-my-pi/pi-utils/dirs";
import { $ } from "bun";
import { contextFileCapability } from "./capability/context-file";
import { systemPromptCapability } from "./capability/system-prompt";
@@ -272,7 +272,7 @@ interface GpuCache {
}
function getSystemInfoCachePath(): string {
return path.join(os.homedir(), ".omp", "gpu_cache.json");
return getGpuCachePath();
}
async function loadGpuCache(): Promise<GpuCache | null> {
+2 -1
View File
@@ -2,6 +2,7 @@ import * as fs from "node:fs/promises";
import * as os from "node:os";
import path from "node:path";
import { isEnoent, Snowflake } from "@oh-my-pi/pi-utils";
import { getWorktreeDir } from "@oh-my-pi/pi-utils/dirs";
import { $ } from "bun";
export interface WorktreeBaseline {
@@ -30,7 +31,7 @@ export async function getRepoRoot(cwd: string): Promise<string> {
export async function ensureWorktree(baseCwd: string, id: string): Promise<string> {
const repoRoot = await getRepoRoot(baseCwd);
const encodedProject = getEncodedProjectName(repoRoot);
const worktreeDir = path.join(os.homedir(), ".omp", "wt", encodedProject, id);
const worktreeDir = getWorktreeDir(encodedProject, id);
await fs.mkdir(path.dirname(worktreeDir), { recursive: true });
await $`git worktree remove -f ${worktreeDir}`.cwd(repoRoot).quiet().nothrow();
await fs.rm(worktreeDir, { recursive: true, force: true });
+2 -3
View File
@@ -1,5 +1,4 @@
import * as fs from "node:fs/promises";
import * as os from "node:os";
import path from "node:path";
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
@@ -7,8 +6,8 @@ import { FileType, glob } from "@oh-my-pi/pi-natives";
import type { Component } from "@oh-my-pi/pi-tui";
import { Text } from "@oh-my-pi/pi-tui";
import { ptree, untilAborted } from "@oh-my-pi/pi-utils";
import { getRemoteDir } from "@oh-my-pi/pi-utils/dirs";
import { type Static, Type } from "@sinclair/typebox";
import { CONFIG_DIR_NAME } from "../config";
import { renderPromptTemplate } from "../config/prompt-templates";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
@@ -40,7 +39,7 @@ import {
const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]);
// Remote mount path prefix (sshfs mounts) - skip fuzzy matching to avoid hangs
const REMOTE_MOUNT_PREFIX = path.join(os.homedir(), CONFIG_DIR_NAME, "remote") + path.sep;
const REMOTE_MOUNT_PREFIX = getRemoteDir() + path.sep;
function isRemoteMountPath(absolutePath: string): boolean {
return absolutePath.startsWith(REMOTE_MOUNT_PREFIX);
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { logger, ptree, TempDir } from "@oh-my-pi/pi-utils";
import { APP_NAME, getToolsDir } from "../config";
import { APP_NAME, getToolsDir } from "@oh-my-pi/pi-utils/dirs";
const TOOLS_DIR = getToolsDir();
const TOOL_DOWNLOAD_TIMEOUT_MS = 15000;
+1 -1
View File
@@ -9,7 +9,7 @@
*/
import { buildAnthropicHeaders as buildProviderAnthropicHeaders, getEnvApiKey } from "@oh-my-pi/pi-ai";
import { $env, logger } from "@oh-my-pi/pi-utils";
import { getAgentDbPath, getAgentDir } from "../../config";
import { getAgentDbPath, getAgentDir } from "@oh-my-pi/pi-utils/dirs";
import { AgentStorage } from "../../session/agent-storage";
import type { AuthCredential } from "../../session/auth-storage";
import type { AnthropicAuthConfig, AnthropicOAuthCredential, ModelsJson } from "./types";
@@ -7,8 +7,8 @@
*/
import * as os from "node:os";
import { readSseJson } from "@oh-my-pi/pi-utils";
import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs";
import packageJson from "../../../../package.json" with { type: "json" };
import { getAgentDbPath } from "../../../config";
import { AgentStorage } from "../../../session/agent-storage";
import type { SearchResponse, SearchSource } from "../../../web/search/types";
import { SearchProviderError } from "../../../web/search/types";
@@ -6,7 +6,7 @@
* Returns synthesized answers with citations and source metadata from grounding chunks.
*/
import { refreshGoogleCloudToken } from "@oh-my-pi/pi-ai";
import { getAgentDbPath } from "../../../config";
import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs";
import { AgentStorage } from "../../../session/agent-storage";
import type { SearchCitation, SearchResponse, SearchSource } from "../../../web/search/types";
import { SearchProviderError } from "../../../web/search/types";
@@ -8,7 +8,7 @@
import { getEnvApiKey } from "@oh-my-pi/pi-ai";
import { readSseJson } from "@oh-my-pi/pi-utils";
import { getAgentDbPath } from "../../../config";
import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs";
import { AgentStorage } from "../../../session/agent-storage";
import type {
PerplexityMessageOutput,
@@ -1,7 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
import * as fs from "node:fs";
import * as path from "node:path";
import { Agent } from "@oh-my-pi/pi-agent-core";
import { getModel } from "@oh-my-pi/pi-ai/models";
import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
@@ -12,6 +11,7 @@ import { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session";
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
import { TempDir } from "@oh-my-pi/pi-utils";
import { getProjectAgentDir } from "@oh-my-pi/pi-utils/dirs";
/**
* Regression test: auto-compaction completion should resume the agent loop when
@@ -30,7 +30,7 @@ describe("AgentSession auto-compaction queue resume", () => {
// Provide an extension that short-circuits compaction so the test doesn't
// make any LLM calls.
const extensionsDir = path.join(tempDir.path(), ".omp", "extensions");
const extensionsDir = path.join(getProjectAgentDir(tempDir.path()), "extensions");
fs.mkdirSync(extensionsDir, { recursive: true });
fs.writeFileSync(
path.join(extensionsDir, "compaction-short-circuit.ts"),
@@ -3,6 +3,7 @@ import * as fs from "node:fs/promises";
import * as path from "node:path";
import { loadPythonModules, type PythonModuleExecutor } from "@oh-my-pi/pi-coding-agent/ipy/modules";
import { TempDir } from "@oh-my-pi/pi-utils";
import { getAgentModulesDir, getProjectModulesDir } from "@oh-my-pi/pi-utils/dirs";
const fixturesDir = path.resolve(import.meta.dir, "../../test/fixtures/python-modules");
@@ -26,11 +27,11 @@ describe("python modules", () => {
it("loads modules in sorted order with silent execution", async () => {
tempRoot = TempDir.createSync("@omp-python-modules-");
const homeDir = path.join(tempRoot.path(), "home");
const agentDir = path.join(tempRoot.path(), "agent");
const cwd = path.join(tempRoot.path(), "project");
await writeModule(path.join(homeDir, ".omp", "agent", "modules"), "beta.py", "user-omp");
await writeModule(path.join(homeDir, ".omp", "agent", "modules"), "alpha.py", "user-omp");
await writeModule(getAgentModulesDir(agentDir), "beta.py", "user-omp");
await writeModule(getAgentModulesDir(agentDir), "alpha.py", "user-omp");
const calls: Array<{ name: string; options?: { silent?: boolean; storeHistory?: boolean } }> = [];
const executor: PythonModuleExecutor = {
@@ -41,7 +42,7 @@ describe("python modules", () => {
},
};
await loadPythonModules(executor, { cwd, homeDir });
await loadPythonModules(executor, { cwd, agentDir });
expect(calls.map(call => call.name)).toEqual(["alpha", "beta"]);
for (const call of calls) {
expect(call.options).toEqual({ silent: true, storeHistory: false });
@@ -50,11 +51,11 @@ describe("python modules", () => {
it("fails fast when a module fails to execute", async () => {
tempRoot = TempDir.createSync("@omp-python-modules-");
const homeDir = path.join(tempRoot.path(), "home");
const agentDir = path.join(tempRoot.path(), "agent");
const cwd = path.join(tempRoot.path(), "project");
await writeModule(path.join(homeDir, ".omp", "agent", "modules"), "alpha.py", "user-omp");
await writeModule(path.join(cwd, ".omp", "modules"), "beta.py", "project-omp");
await writeModule(getAgentModulesDir(agentDir), "alpha.py", "user-omp");
await writeModule(getProjectModulesDir(cwd), "beta.py", "project-omp");
const executor: PythonModuleExecutor = {
execute: async (code: string) => {
@@ -69,6 +70,6 @@ describe("python modules", () => {
},
};
await expect(loadPythonModules(executor, { cwd, homeDir })).rejects.toThrow("Failed to load Python module");
await expect(loadPythonModules(executor, { cwd, agentDir })).rejects.toThrow("Failed to load Python module");
});
});
@@ -4,6 +4,7 @@ import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { discoverAndLoadExtensions, loadExtensions } from "@oh-my-pi/pi-coding-agent/extensibility/extensions/loader";
import { TempDir } from "@oh-my-pi/pi-utils";
import { getProjectAgentDir } from "@oh-my-pi/pi-utils/dirs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -13,7 +14,7 @@ describe("extensions discovery", () => {
beforeEach(() => {
tempDir = TempDir.createSync("@pi-ext-test-");
extensionsDir = path.join(tempDir.path(), ".omp", "extensions");
extensionsDir = path.join(getProjectAgentDir(tempDir.path()), "extensions");
fs.mkdirSync(extensionsDir, { recursive: true });
});
@@ -11,6 +11,7 @@ import { ExtensionRunner } from "@oh-my-pi/pi-coding-agent/extensibility/extensi
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
import { logger, TempDir } from "@oh-my-pi/pi-utils";
import { getProjectAgentDir } from "@oh-my-pi/pi-utils/dirs";
describe("ExtensionRunner", () => {
let tempDir: TempDir;
@@ -20,7 +21,7 @@ describe("ExtensionRunner", () => {
beforeEach(async () => {
tempDir = TempDir.createSync("@pi-runner-test-");
extensionsDir = path.join(tempDir.path(), ".omp", "extensions");
extensionsDir = path.join(getProjectAgentDir(tempDir.path()), "extensions");
fs.mkdirSync(extensionsDir, { recursive: true });
sessionManager = SessionManager.inMemory();
const authStorage = await AuthStorage.create(path.join(tempDir.path(), "testauth.db"));
@@ -5,6 +5,7 @@ import * as path from "node:path";
import type { Skill } from "@oh-my-pi/pi-coding-agent/sdk";
import { createAgentSession } from "@oh-my-pi/pi-coding-agent/sdk";
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
import { getProjectAgentDir } from "@oh-my-pi/pi-utils/dirs";
describe("createAgentSession skills option", () => {
let tempDir: string;
@@ -13,7 +14,7 @@ describe("createAgentSession skills option", () => {
beforeEach(() => {
tempDir = path.join(os.tmpdir(), `pi-sdk-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
// Create skill in .omp/skills/ for native project-level discovery
skillsDir = path.join(tempDir, ".omp", "skills", "test-skill");
skillsDir = path.join(getProjectAgentDir(tempDir), "skills", "test-skill");
fs.mkdirSync(skillsDir, { recursive: true });
// Create a test skill in the pi skills directory
@@ -4,6 +4,7 @@ import * as os from "node:os";
import * as path from "node:path";
import { _resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { Snowflake } from "@oh-my-pi/pi-utils";
import { getProjectAgentDir } from "@oh-my-pi/pi-utils/dirs";
import { YAML } from "bun";
describe("Settings", () => {
@@ -24,7 +25,7 @@ describe("Settings", () => {
fs.rmSync(testDir, { recursive: true });
}
fs.mkdirSync(agentDir, { recursive: true });
fs.mkdirSync(path.join(projectDir, ".omp"), { recursive: true });
fs.mkdirSync(getProjectAgentDir(projectDir), { recursive: true });
});
const getConfigPath = () => path.join(agentDir, "config.yml");
+66 -65
View File
@@ -1,66 +1,67 @@
{
"name": "@oh-my-pi/omp-stats",
"version": "12.0.0",
"description": "Local observability dashboard for pi AI usage statistics",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"bin": {
"omp-stats": "./src/index.ts"
},
"exports": {
".": {
"types": "./src/index.ts",
"import": "./src/index.ts"
},
"./src/server": {
"types": "./src/server.ts",
"import": "./src/server.ts"
},
"./src/db": {
"types": "./src/db.ts",
"import": "./src/db.ts"
}
},
"files": [
"src",
"public",
"README.md"
],
"keywords": [
"ai",
"observability",
"metrics",
"dashboard",
"llm",
"statistics"
],
"scripts": {
"dev": "bun run src/index.ts",
"check": "tsgo -p tsconfig.json && tsgo -p tsconfig.client.json",
"build": "bun run build.ts"
},
"author": "Can Bölük",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/can1357/oh-my-pi.git",
"directory": "packages/stats"
},
"dependencies": {
"@oh-my-pi/pi-ai": "workspace:*",
"date-fns": "^4.1.0",
"lucide-react": "^0.563.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"recharts": "^3.7.0"
},
"devDependencies": {
"@types/bun": "^1.3.9",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3"
},
"engines": {
"bun": ">=1.3.7"
}
}
"name": "@oh-my-pi/omp-stats",
"version": "12.0.0",
"description": "Local observability dashboard for pi AI usage statistics",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"bin": {
"omp-stats": "./src/index.ts"
},
"exports": {
".": {
"types": "./src/index.ts",
"import": "./src/index.ts"
},
"./src/server": {
"types": "./src/server.ts",
"import": "./src/server.ts"
},
"./src/db": {
"types": "./src/db.ts",
"import": "./src/db.ts"
}
},
"files": [
"src",
"public",
"README.md"
],
"keywords": [
"ai",
"observability",
"metrics",
"dashboard",
"llm",
"statistics"
],
"scripts": {
"dev": "bun run src/index.ts",
"check": "tsgo -p tsconfig.json && tsgo -p tsconfig.client.json",
"build": "bun run build.ts"
},
"author": "Can Bölük",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/can1357/oh-my-pi.git",
"directory": "packages/stats"
},
"dependencies": {
"@oh-my-pi/pi-ai": "workspace:*",
"date-fns": "^4.1.0",
"lucide-react": "^0.563.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"recharts": "^3.7.0",
"@oh-my-pi/pi-utils": "workspace:*"
},
"devDependencies": {
"@types/bun": "^1.3.9",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3"
},
"engines": {
"bun": ">=1.3.7"
}
}
+3 -4
View File
@@ -1,7 +1,6 @@
import { Database } from "bun:sqlite";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { getConfigRootDir, getStatsDbPath } from "@oh-my-pi/pi-utils/dirs";
import type {
AggregatedStats,
FolderStats,
@@ -12,7 +11,7 @@ import type {
TimeSeriesPoint,
} from "./types";
const DB_PATH = path.join(os.homedir(), ".omp", "stats.db");
const DB_PATH = getStatsDbPath();
let db: Database | null = null;
@@ -23,7 +22,7 @@ export async function initDb(): Promise<Database> {
if (db) return db;
// Ensure directory exists
await fs.mkdir(path.join(os.homedir(), ".omp"), { recursive: true });
await fs.mkdir(getConfigRootDir(), { recursive: true });
db = new Database(DB_PATH);
db.exec("PRAGMA journal_mode = WAL");
+4 -12
View File
@@ -1,11 +1,9 @@
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
import { getSessionsDir } from "@oh-my-pi/pi-utils/dirs";
import type { MessageStats, SessionEntry, SessionMessageEntry } from "./types";
const SESSIONS_DIR = path.join(os.homedir(), ".omp", "agent", "sessions");
/**
* Extract folder name from session filename.
* Session files are named like: --work--pi--/timestamp_uuid.jsonl
@@ -95,8 +93,9 @@ export async function parseSessionFile(
*/
export async function listSessionFolders(): Promise<string[]> {
try {
const entries = await fs.readdir(SESSIONS_DIR, { withFileTypes: true });
return entries.filter(e => e.isDirectory()).map(e => path.join(SESSIONS_DIR, e.name));
const sessionsDir = getSessionsDir();
const entries = await fs.readdir(sessionsDir, { withFileTypes: true });
return entries.filter(e => e.isDirectory()).map(e => path.join(sessionsDir, e.name));
} catch {
return [];
}
@@ -129,13 +128,6 @@ export async function listAllSessionFiles(): Promise<string[]> {
return allFiles;
}
/**
* Get session directory path.
*/
export function getSessionsDir(): string {
return SESSIONS_DIR;
}
/**
* Find a specific entry in a session file.
*/
+3 -3
View File
@@ -2,8 +2,8 @@
* Minimal TUI implementation with differential rendering
*/
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { getCrashLogPath, getDebugLogPath } from "@oh-my-pi/pi-utils/dirs";
import { isKeyRelease, matchesKey } from "./keys";
import type { Terminal } from "./terminal";
import { setCellDimensions, TERMINAL } from "./terminal-capabilities";
@@ -872,7 +872,7 @@ export class TUI extends Container {
const debugRedraw = process.env.PI_DEBUG_REDRAW === "1";
const logRedraw = (reason: string): void => {
if (!debugRedraw) return;
const logPath = path.join(os.homedir(), ".omp", "agent", "omp-debug.log");
const logPath = getDebugLogPath();
const msg = `[${new Date().toISOString()}] fullRender: ${reason} (prev=${this.#previousLines.length}, new=${newLines.length}, height=${height})\n`;
fs.appendFileSync(logPath, msg);
};
@@ -1018,7 +1018,7 @@ export class TUI extends Container {
const isImage = TERMINAL.isImageLine(line);
if (!isImage && visibleWidth(line) > width) {
// Log all lines to crash file for debugging
const crashLogPath = path.join(os.homedir(), ".omp", "agent", "omp-crash.log");
const crashLogPath = getCrashLogPath();
const crashData = [
`Crash at ${new Date().toISOString()}`,
`Terminal width: ${width}`,
+4
View File
@@ -13,6 +13,10 @@
"./cli": {
"types": "./src/cli.ts",
"import": "./src/cli.ts"
},
"./dirs": {
"types": "./src/dirs.ts",
"import": "./src/dirs.ts"
}
},
"files": [
+250
View File
@@ -0,0 +1,250 @@
/**
* 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.
*/
import * as os from "node:os";
import * as path from "node:path";
import packageJson from "../../coding-agent/package.json" with { type: "json" };
import { $env } from "./env";
/** App name (e.g. "omp") */
export const APP_NAME: string = packageJson.ompConfig?.name || "omp";
/** Config directory name (e.g. ".omp") */
export const CONFIG_DIR_NAME: string = packageJson.ompConfig?.configDir || ".omp";
/** Version (e.g. "1.0.0") */
export const VERSION: string = packageJson.version;
/**
* 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");
}
/**
* Get the path to the project-local agent directory.
*/
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");
}
/**
* Get the logs directory (~/.omp/logs).
*/
export function getLogsDir(): string {
return path.join(getConfigRootDir(), "logs");
}
/**
* 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")];
}
return [path.join(getProjectAgentDir(cwd), "mcp.json"), path.join(getProjectAgentDir(cwd), ".mcp.json")];
}
/**
* 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];
}
/**
* Get the worktree base directory (~/.omp/wt).
*/
export function getWorktreeBaseDir(): string {
return path.join(getConfigRootDir(), "wt");
}
/**
* Get the path to a worktree directory.
*/
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 path.join(getConfigRootDir(), "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).
*/
export function getNativesDir(): string {
return path.join(getConfigRootDir(), "natives");
}
/**
* Get the stats database path (~/.omp/stats.db).
*/
export function getStatsDbPath(): string {
return path.join(getConfigRootDir(), "stats.db");
}
/**
* Get the crash log path (~/.omp/agent/omp-crash.log).
*/
export function getCrashLogPath(): string {
return path.join(getAgentDir(), "omp-crash.log");
}
/** 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 path to user's custom themes directory */
export function getCustomThemesDir(): string {
return path.join(getAgentDir(), "themes");
}
/** Get path to tools directory */
export function getToolsDir(): string {
return path.join(getAgentDir(), "tools");
}
/** Get path to slash commands directory */
export function getCommandsDir(): string {
return path.join(getAgentDir(), "commands");
}
/** Get path to prompts directory */
export function getPromptsDir(): string {
return path.join(getAgentDir(), "prompts");
}
/** Get path to content-addressed blob store directory */
export function getBlobsDir(): string {
return path.join(getAgentDir(), "blobs");
}
/** Get path to debug log file */
export function getDebugLogPath(): string {
return path.join(getAgentDir(), `${APP_NAME}-debug.log`);
}
+1 -7
View File
@@ -5,15 +5,9 @@
* Each log entry includes process.pid for traceability.
*/
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import winston from "winston";
import DailyRotateFile from "winston-daily-rotate-file";
/** Get the logs directory (~/.omp/logs/) */
function getLogsDir(): string {
return path.join(os.homedir(), ".omp", "logs");
}
import { getLogsDir } from "./dirs";
/** Ensure logs directory exists */
function ensureLogsDir(): string {