diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 57328418..9cb5a614 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +### Added + +- **Forking/Rebranding Support**: All branding (app name, config directory, environment variable names) is now configurable via `piConfig` in `package.json`. Forks can change `piConfig.name` and `piConfig.configDir` to rebrand the CLI without code changes. Affects CLI banner, help text, config paths, and error messages. ([#95](https://github.com/badlogic/pi-mono/pull/95)) + +### Fixed + +- **Bun Binary Detection**: Fixed Bun compiled binary failing to start after Bun updated its virtual filesystem path format from `%7EBUN` to `$bunfs`. ([#95](https://github.com/badlogic/pi-mono/pull/95)) + ## [0.12.4] - 2025-12-02 ### Added diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index aa9a75e9..ba7f1022 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -1141,6 +1141,30 @@ Things that might happen eventually: ## Development +### Forking / Rebranding + +All branding (app name, config directory) is configurable via `package.json`: + +```json +{ + "piConfig": { + "name": "pi", + "configDir": ".pi" + } +} +``` + +To create a fork with different branding: +1. Change `piConfig.name` to your app name (e.g., `"tau"`) +2. Change `piConfig.configDir` to your config directory (e.g., `".tau"`) +3. Change the `bin` field to your command name: `"bin": { "tau": "dist/cli.js" }` + +This affects: +- CLI banner and help text +- Config directory (`~/.pi/agent/` → `~/.tau/agent/`) +- Environment variable name (`PI_CODING_AGENT_DIR` → `TAU_CODING_AGENT_DIR`) +- All user-facing paths in error messages and documentation + ### Path Resolution The codebase supports three execution modes: diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 56abf41a..acc31a4c 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -3,6 +3,10 @@ "version": "0.12.4", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", + "piConfig": { + "name": "pi", + "configDir": ".pi" + }, "bin": { "pi": "dist/cli.js" }, diff --git a/packages/coding-agent/src/changelog.ts b/packages/coding-agent/src/changelog.ts index 32eb2997..1b13b6b8 100644 --- a/packages/coding-agent/src/changelog.ts +++ b/packages/coding-agent/src/changelog.ts @@ -96,4 +96,4 @@ export function getNewEntries(entries: ChangelogEntry[], lastVersion: string): C } // Re-export getChangelogPath from paths.ts for convenience -export { getChangelogPath } from "./paths.js"; +export { getChangelogPath } from "./config.js"; diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts new file mode 100644 index 00000000..bcb5f711 --- /dev/null +++ b/packages/coding-agent/src/config.ts @@ -0,0 +1,131 @@ +import { existsSync, readFileSync } from "fs"; +import { homedir } from "os"; +import { dirname, join, resolve } from "path"; +import { fileURLToPath } from "url"; + +// ============================================================================= +// Package Detection +// ============================================================================= + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +/** + * Detect if we're running as a Bun compiled binary. + * Bun binaries have import.meta.url containing "$bunfs" (Bun's virtual filesystem path) + */ +export const isBunBinary = import.meta.url.includes("$bunfs"); + +// ============================================================================= +// Package Asset Paths (shipped with executable) +// ============================================================================= + +/** + * Get the base directory for resolving package assets (themes, package.json, README.md, CHANGELOG.md). + * - For Bun binary: returns the directory containing the executable + * - For Node.js (dist/): returns __dirname (the dist/ directory) + * - For tsx (src/): returns parent directory (the package root) + */ +export function getPackageDir(): string { + if (isBunBinary) { + // Bun binary: process.execPath points to the compiled executable + return dirname(process.execPath); + } + // Node.js: check if package.json exists in __dirname (dist/) or parent (src/ case) + if (existsSync(join(__dirname, "package.json"))) { + return __dirname; + } + // Running from src/ via tsx - go up one level to package root + return dirname(__dirname); +} + +/** + * Get path to built-in themes directory (shipped with package) + * - For Bun binary: theme/ next to executable + * - For Node.js (dist/): dist/theme/ + * - For tsx (src/): src/theme/ + */ +export function getThemesDir(): string { + if (isBunBinary) { + return join(dirname(process.execPath), "theme"); + } + // __dirname is either dist/ or src/ - theme is always a subdirectory + return join(__dirname, "theme"); +} + +/** Get path to package.json */ +export function getPackageJsonPath(): string { + return join(getPackageDir(), "package.json"); +} + +/** Get path to README.md */ +export function getReadmePath(): string { + return resolve(join(getPackageDir(), "README.md")); +} + +/** Get path to CHANGELOG.md */ +export function getChangelogPath(): string { + return resolve(join(getPackageDir(), "CHANGELOG.md")); +} + +// ============================================================================= +// App Config (from package.json piConfig) +// ============================================================================= + +const pkg = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8")); + +export const APP_NAME: string = pkg.piConfig?.name || "pi"; +export const CONFIG_DIR_NAME: string = pkg.piConfig?.configDir || ".pi"; +export const VERSION: string = pkg.version; + +// e.g., PI_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR +export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`; + +// ============================================================================= +// User Config Paths (~/.pi/agent/*) +// ============================================================================= + +/** Get the agent config directory (e.g., ~/.pi/agent/) */ +export function getAgentDir(): string { + return process.env[ENV_AGENT_DIR] || join(homedir(), CONFIG_DIR_NAME, "agent"); +} + +/** Get path to user's custom themes directory */ +export function getCustomThemesDir(): string { + return join(getAgentDir(), "themes"); +} + +/** Get path to models.json */ +export function getModelsPath(): string { + return join(getAgentDir(), "models.json"); +} + +/** Get path to oauth.json */ +export function getOAuthPath(): string { + return join(getAgentDir(), "oauth.json"); +} + +/** Get path to settings.json */ +export function getSettingsPath(): string { + return join(getAgentDir(), "settings.json"); +} + +/** Get path to tools directory */ +export function getToolsDir(): string { + return join(getAgentDir(), "tools"); +} + +/** Get path to slash commands directory */ +export function getCommandsDir(): string { + return join(getAgentDir(), "commands"); +} + +/** Get path to sessions directory */ +export function getSessionsDir(): string { + return join(getAgentDir(), "sessions"); +} + +/** Get path to debug log file */ +export function getDebugLogPath(): string { + return join(getAgentDir(), `${APP_NAME}-debug.log`); +} diff --git a/packages/coding-agent/src/export-html.ts b/packages/coding-agent/src/export-html.ts index 43078341..9758bcab 100644 --- a/packages/coding-agent/src/export-html.ts +++ b/packages/coding-agent/src/export-html.ts @@ -3,13 +3,9 @@ import type { AssistantMessage, Message, ToolResultMessage, UserMessage } from " import { existsSync, readFileSync, writeFileSync } from "fs"; import { homedir } from "os"; import { basename } from "path"; -import { getPackageJsonPath } from "./paths.js"; +import { VERSION } from "./config.js"; import type { SessionManager } from "./session-manager.js"; -// Get version from package.json -const packageJson = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8")); -const VERSION = packageJson.version; - /** * TUI Color scheme (matching exact RGB values from TUI components) */ diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 2b199435..c8b278b7 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -6,9 +6,17 @@ import { existsSync, readFileSync, statSync } from "fs"; import { homedir } from "os"; import { extname, join, resolve } from "path"; import { getChangelogPath, getNewEntries, parseChangelog } from "./changelog.js"; +import { + APP_NAME, + CONFIG_DIR_NAME, + ENV_AGENT_DIR, + getAgentDir, + getModelsPath, + getReadmePath, + VERSION, +} from "./config.js"; import { exportFromFile } from "./export-html.js"; import { findModel, getApiKeyForModel, getAvailableModels } from "./model-config.js"; -import { getPackageJsonPath, getReadmePath } from "./paths.js"; import { SessionManager } from "./session-manager.js"; import { SettingsManager } from "./settings-manager.js"; import { expandSlashCommand, loadSlashCommands } from "./slash-commands.js"; @@ -18,10 +26,6 @@ import { ensureTool } from "./tools-manager.js"; import { SessionSelectorComponent } from "./tui/session-selector.js"; import { TuiRenderer } from "./tui/tui-renderer.js"; -// Get version from package.json -const packageJson = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8")); -const VERSION = packageJson.version; - const defaultModelPerProvider: Record = { anthropic: "claude-sonnet-4-5", openai: "gpt-5.1-codex", @@ -220,10 +224,10 @@ function processFileArguments(fileArgs: string[]): { textContent: string; imageA } function printHelp() { - console.log(`${chalk.bold("pi")} - AI coding assistant with read, bash, edit, write tools + console.log(`${chalk.bold(APP_NAME)} - AI coding assistant with read, bash, edit, write tools ${chalk.bold("Usage:")} - pi [options] [@files...] [messages...] + ${APP_NAME} [options] [@files...] [messages...] ${chalk.bold("Options:")} --provider Provider name (default: google) @@ -245,41 +249,41 @@ ${chalk.bold("Options:")} ${chalk.bold("Examples:")} # Interactive mode - pi + ${APP_NAME} # Interactive mode with initial prompt - pi "List all .ts files in src/" + ${APP_NAME} "List all .ts files in src/" # Include files in initial message - pi @prompt.md @image.png "What color is the sky?" + ${APP_NAME} @prompt.md @image.png "What color is the sky?" # Non-interactive mode (process and exit) - pi -p "List all .ts files in src/" + ${APP_NAME} -p "List all .ts files in src/" # Multiple messages (interactive) - pi "Read package.json" "What dependencies do we have?" + ${APP_NAME} "Read package.json" "What dependencies do we have?" # Continue previous session - pi --continue "What did we discuss?" + ${APP_NAME} --continue "What did we discuss?" # Use different model - pi --provider openai --model gpt-4o-mini "Help me refactor this code" + ${APP_NAME} --provider openai --model gpt-4o-mini "Help me refactor this code" # Limit model cycling to specific models - pi --models claude-sonnet,claude-haiku,gpt-4o + ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o # Cycle models with fixed thinking levels - pi --models sonnet:high,haiku:low + ${APP_NAME} --models sonnet:high,haiku:low # Start with a specific thinking level - pi --thinking high "Solve this complex problem" + ${APP_NAME} --thinking high "Solve this complex problem" # Read-only mode (no file modifications possible) - pi --tools read,grep,find,ls -p "Review the code in src/" + ${APP_NAME} --tools read,grep,find,ls -p "Review the code in src/" # Export a session file to HTML - pi --export ~/.pi/agent/sessions/--path--/session.jsonl - pi --export session.jsonl output.html + ${APP_NAME} --export ~/${CONFIG_DIR_NAME}/agent/sessions/--path--/session.jsonl + ${APP_NAME} --export session.jsonl output.html ${chalk.bold("Environment Variables:")} ANTHROPIC_API_KEY - Anthropic Claude API key @@ -291,7 +295,7 @@ ${chalk.bold("Environment Variables:")} XAI_API_KEY - xAI Grok API key OPENROUTER_API_KEY - OpenRouter API key ZAI_API_KEY - ZAI API key - PI_CODING_AGENT_DIR - Session storage directory (default: ~/.pi/agent) + ${ENV_AGENT_DIR.padEnd(23)} - Session storage directory (default: ~/${CONFIG_DIR_NAME}/agent) ${chalk.bold("Available Tools (default: read, bash, edit, write):")} read - Read file contents @@ -488,16 +492,15 @@ function loadContextFileFromDir(dir: string): { path: string; content: string } /** * Load all project context files in order: - * 1. Global: ~/.pi/agent/AGENTS.md or CLAUDE.md + * 1. Global: ~/{CONFIG_DIR_NAME}/agent/AGENTS.md or CLAUDE.md * 2. Parent directories (top-most first) down to cwd * Each returns {path, content} for separate messages */ function loadProjectContextFiles(): Array<{ path: string; content: string }> { const contextFiles: Array<{ path: string; content: string }> = []; - // 1. Load global context from ~/.pi/agent/ - const homeDir = homedir(); - const globalContextDir = resolve(process.env.PI_CODING_AGENT_DIR || join(homeDir, ".pi/agent/")); + // 1. Load global context from ~/{CONFIG_DIR_NAME}/agent/ + const globalContextDir = getAgentDir(); const globalContext = loadContextFileFromDir(globalContextDir); if (globalContext) { contextFiles.push(globalContext); @@ -1028,7 +1031,7 @@ export async function main(args: string[]) { console.error(chalk.red("No models available.")); console.error(chalk.yellow("\nSet an API key environment variable:")); console.error(" ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, etc."); - console.error(chalk.yellow("\nOr create ~/.pi/agent/models.json")); + console.error(chalk.yellow(`\nOr create ${getModelsPath()}`)); process.exit(1); } @@ -1108,7 +1111,7 @@ export async function main(args: string[]) { console.error(chalk.red("\nNo models available.")); console.error(chalk.yellow("Set an API key environment variable:")); console.error(" ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, etc."); - console.error(chalk.yellow("\nOr create ~/.pi/agent/models.json")); + console.error(chalk.yellow(`\nOr create ${getModelsPath()}`)); } process.exit(1); } @@ -1153,7 +1156,7 @@ export async function main(args: string[]) { const key = await getApiKeyForModel(currentModel); if (!key) { throw new Error( - `No API key found for provider "${currentModel.provider}". Please set the appropriate environment variable or update ~/.pi/agent/models.json`, + `No API key found for provider "${currentModel.provider}". Please set the appropriate environment variable or update ${getModelsPath()}`, ); } return key; diff --git a/packages/coding-agent/src/model-config.ts b/packages/coding-agent/src/model-config.ts index 62248dbe..b5265028 100644 --- a/packages/coding-agent/src/model-config.ts +++ b/packages/coding-agent/src/model-config.ts @@ -2,8 +2,7 @@ import { type Api, getApiKey, getModels, getProviders, type KnownProvider, type import { type Static, Type } from "@sinclair/typebox"; import AjvModule from "ajv"; import { existsSync, readFileSync } from "fs"; -import { homedir } from "os"; -import { join } from "path"; +import { getModelsPath } from "./config.js"; import { getOAuthToken, type SupportedOAuthProvider } from "./oauth/index.js"; import { loadOAuthCredentials } from "./oauth/storage.js"; @@ -75,11 +74,11 @@ export function resolveApiKey(keyConfig: string): string | undefined { } /** - * Load custom models from ~/.pi/agent/models.json + * Load custom models from models.json in agent config dir * Returns { models, error } - either models array or error message */ function loadCustomModels(): { models: Model[]; error: string | null } { - const configPath = join(homedir(), ".pi", "agent", "models.json"); + const configPath = getModelsPath(); if (!existsSync(configPath)) { return { models: [], error: null }; } diff --git a/packages/coding-agent/src/oauth/storage.ts b/packages/coding-agent/src/oauth/storage.ts index c4662b84..f994949f 100644 --- a/packages/coding-agent/src/oauth/storage.ts +++ b/packages/coding-agent/src/oauth/storage.ts @@ -1,6 +1,5 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; -import { homedir } from "os"; -import { join } from "path"; +import { getAgentDir, getOAuthPath } from "../config.js"; export interface OAuthCredentials { type: "oauth"; @@ -13,19 +12,11 @@ interface OAuthStorageFormat { [provider: string]: OAuthCredentials; } -/** - * Get path to oauth.json - */ -function getOAuthFilePath(): string { - const configDir = join(homedir(), ".pi", "agent"); - return join(configDir, "oauth.json"); -} - /** * Ensure the config directory exists */ function ensureConfigDir(): void { - const configDir = join(homedir(), ".pi", "agent"); + const configDir = getAgentDir(); if (!existsSync(configDir)) { mkdirSync(configDir, { recursive: true, mode: 0o700 }); } @@ -35,7 +26,7 @@ function ensureConfigDir(): void { * Load all OAuth credentials from oauth.json */ function loadStorage(): OAuthStorageFormat { - const filePath = getOAuthFilePath(); + const filePath = getOAuthPath(); if (!existsSync(filePath)) { return {}; } @@ -54,7 +45,7 @@ function loadStorage(): OAuthStorageFormat { */ function saveStorage(storage: OAuthStorageFormat): void { ensureConfigDir(); - const filePath = getOAuthFilePath(); + const filePath = getOAuthPath(); writeFileSync(filePath, JSON.stringify(storage, null, 2), "utf-8"); // Set permissions to owner read/write only chmodSync(filePath, 0o600); diff --git a/packages/coding-agent/src/paths.ts b/packages/coding-agent/src/paths.ts deleted file mode 100644 index f6c47e50..00000000 --- a/packages/coding-agent/src/paths.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { existsSync } from "fs"; -import { dirname, join, resolve } from "path"; -import { fileURLToPath } from "url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -/** - * Detect if we're running as a Bun compiled binary. - * Bun binaries have import.meta.url containing "%7EBUN" (URL-encoded ~BUN virtual filesystem path) - */ -export const isBunBinary = import.meta.url.includes("%7EBUN"); - -/** - * Type definition for Bun global (only available when running via Bun) - */ -declare const Bun: { main: string } | undefined; - -/** - * Get the base directory for resolving package assets (themes, package.json, README.md, CHANGELOG.md). - * - For Bun binary: returns the directory containing the executable - * - For Node.js (dist/): returns __dirname (the dist/ directory) - * - For tsx (src/): returns parent directory (the package root) - */ -export function getPackageDir(): string { - if (isBunBinary) { - // Bun binary: process.execPath points to the compiled executable - return dirname(process.execPath); - } - // Node.js: check if package.json exists in __dirname (dist/) or parent (src/ case) - if (existsSync(join(__dirname, "package.json"))) { - return __dirname; - } - // Running from src/ via tsx - go up one level to package root - return dirname(__dirname); -} - -/** - * Get path to the theme directory - * - For Bun binary: theme/ next to executable - * - For Node.js (dist/): dist/theme/ - * - For tsx (src/): src/theme/ - */ -export function getThemeDir(): string { - if (isBunBinary) { - return join(dirname(process.execPath), "theme"); - } - // __dirname is either dist/ or src/ - theme is always a subdirectory - return join(__dirname, "theme"); -} - -/** - * Get path to package.json - */ -export function getPackageJsonPath(): string { - return join(getPackageDir(), "package.json"); -} - -/** - * Get path to README.md - */ -export function getReadmePath(): string { - return resolve(join(getPackageDir(), "README.md")); -} - -/** - * Get path to CHANGELOG.md - */ -export function getChangelogPath(): string { - return resolve(join(getPackageDir(), "CHANGELOG.md")); -} diff --git a/packages/coding-agent/src/session-manager.ts b/packages/coding-agent/src/session-manager.ts index 95ff380d..6cd565b6 100644 --- a/packages/coding-agent/src/session-manager.ts +++ b/packages/coding-agent/src/session-manager.ts @@ -1,8 +1,8 @@ import type { AgentState, AppMessage } from "@mariozechner/pi-agent-core"; import { randomBytes } from "crypto"; import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "fs"; -import { homedir } from "os"; import { join, resolve } from "path"; +import { getAgentDir } from "./config.js"; function uuidv4(): string { const bytes = randomBytes(16); @@ -83,7 +83,7 @@ export class SessionManager { // Replace all path separators and colons (for Windows drive letters) with dashes const safePath = "--" + cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-") + "--"; - const configDir = resolve(process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi/agent/")); + const configDir = getAgentDir(); const sessionDir = join(configDir, "sessions", safePath); if (!existsSync(sessionDir)) { mkdirSync(sessionDir, { recursive: true }); diff --git a/packages/coding-agent/src/settings-manager.ts b/packages/coding-agent/src/settings-manager.ts index 93afb112..33461c79 100644 --- a/packages/coding-agent/src/settings-manager.ts +++ b/packages/coding-agent/src/settings-manager.ts @@ -1,6 +1,6 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; -import { homedir } from "os"; import { dirname, join } from "path"; +import { getAgentDir } from "./config.js"; export interface Settings { lastChangelogVersion?: string; @@ -16,7 +16,7 @@ export class SettingsManager { private settings: Settings; constructor(baseDir?: string) { - const dir = baseDir || join(homedir(), ".pi", "agent"); + const dir = baseDir || getAgentDir(); this.settingsPath = join(dir, "settings.json"); this.settings = this.load(); } diff --git a/packages/coding-agent/src/slash-commands.ts b/packages/coding-agent/src/slash-commands.ts index 52384198..552d0f17 100644 --- a/packages/coding-agent/src/slash-commands.ts +++ b/packages/coding-agent/src/slash-commands.ts @@ -1,6 +1,6 @@ import { existsSync, readdirSync, readFileSync } from "fs"; -import { homedir } from "os"; import { join, resolve } from "path"; +import { CONFIG_DIR_NAME, getCommandsDir } from "./config.js"; /** * Represents a custom slash command loaded from a file @@ -167,19 +167,18 @@ function loadCommandsFromDir(dir: string, source: "user" | "project", subdir: st /** * Load all custom slash commands from: - * 1. Global: ~/.pi/agent/commands/ - * 2. Project: ./.pi/commands/ + * 1. Global: ~/{CONFIG_DIR_NAME}/agent/commands/ + * 2. Project: ./{CONFIG_DIR_NAME}/commands/ */ export function loadSlashCommands(): FileSlashCommand[] { const commands: FileSlashCommand[] = []; - // 1. Load global commands from ~/.pi/agent/commands/ - const homeDir = homedir(); - const globalCommandsDir = resolve(process.env.PI_CODING_AGENT_DIR || join(homeDir, ".pi/agent/"), "commands"); + // 1. Load global commands from ~/{CONFIG_DIR_NAME}/agent/commands/ + const globalCommandsDir = getCommandsDir(); commands.push(...loadCommandsFromDir(globalCommandsDir, "user")); - // 2. Load project commands from ./.pi/commands/ - const projectCommandsDir = resolve(process.cwd(), ".pi/commands"); + // 2. Load project commands from ./{CONFIG_DIR_NAME}/commands/ + const projectCommandsDir = resolve(process.cwd(), CONFIG_DIR_NAME, "commands"); commands.push(...loadCommandsFromDir(projectCommandsDir, "project")); return commands; diff --git a/packages/coding-agent/src/theme/theme.ts b/packages/coding-agent/src/theme/theme.ts index 2829764b..287eb522 100644 --- a/packages/coding-agent/src/theme/theme.ts +++ b/packages/coding-agent/src/theme/theme.ts @@ -1,11 +1,10 @@ import * as fs from "node:fs"; -import * as os from "node:os"; import * as path from "node:path"; import type { EditorTheme, MarkdownTheme, SelectListTheme } from "@mariozechner/pi-tui"; import { type Static, Type } from "@sinclair/typebox"; import { TypeCompiler } from "@sinclair/typebox/compiler"; import chalk from "chalk"; -import { getThemeDir } from "../paths.js"; +import { getCustomThemesDir, getThemesDir } from "../config.js"; // ============================================================================ // Types & Schema @@ -323,9 +322,9 @@ let BUILTIN_THEMES: Record | undefined; function getBuiltinThemes(): Record { if (!BUILTIN_THEMES) { - const themeDir = getThemeDir(); - const darkPath = path.join(themeDir, "dark.json"); - const lightPath = path.join(themeDir, "light.json"); + const themesDir = getThemesDir(); + const darkPath = path.join(themesDir, "dark.json"); + const lightPath = path.join(themesDir, "light.json"); BUILTIN_THEMES = { dark: JSON.parse(fs.readFileSync(darkPath, "utf-8")) as ThemeJson, light: JSON.parse(fs.readFileSync(lightPath, "utf-8")) as ThemeJson, @@ -334,15 +333,11 @@ function getBuiltinThemes(): Record { return BUILTIN_THEMES; } -function getThemesDir(): string { - return path.join(os.homedir(), ".pi", "agent", "themes"); -} - export function getAvailableThemes(): string[] { const themes = new Set(Object.keys(getBuiltinThemes())); - const themesDir = getThemesDir(); - if (fs.existsSync(themesDir)) { - const files = fs.readdirSync(themesDir); + const customThemesDir = getCustomThemesDir(); + if (fs.existsSync(customThemesDir)) { + const files = fs.readdirSync(customThemesDir); for (const file of files) { if (file.endsWith(".json")) { themes.add(file.slice(0, -5)); @@ -357,8 +352,8 @@ function loadThemeJson(name: string): ThemeJson { if (name in builtinThemes) { return builtinThemes[name]; } - const themesDir = getThemesDir(); - const themePath = path.join(themesDir, `${name}.json`); + const customThemesDir = getCustomThemesDir(); + const themePath = path.join(customThemesDir, `${name}.json`); if (!fs.existsSync(themePath)) { throw new Error(`Theme not found: ${name}`); } @@ -474,8 +469,8 @@ function startThemeWatcher(): void { return; } - const themesDir = getThemesDir(); - const themeFile = path.join(themesDir, `${currentThemeName}.json`); + const customThemesDir = getCustomThemesDir(); + const themeFile = path.join(customThemesDir, `${currentThemeName}.json`); // Only watch if the file exists if (!fs.existsSync(themeFile)) { diff --git a/packages/coding-agent/src/tools-manager.ts b/packages/coding-agent/src/tools-manager.ts index a93762ac..4915e86c 100644 --- a/packages/coding-agent/src/tools-manager.ts +++ b/packages/coding-agent/src/tools-manager.ts @@ -1,12 +1,13 @@ import chalk from "chalk"; import { spawnSync } from "child_process"; import { chmodSync, createWriteStream, existsSync, mkdirSync, renameSync, rmSync } from "fs"; -import { arch, homedir, platform } from "os"; +import { arch, platform } from "os"; import { join } from "path"; import { Readable } from "stream"; import { finished } from "stream/promises"; +import { getToolsDir } from "./config.js"; -const TOOLS_DIR = join(homedir(), ".pi", "agent", "tools"); +const TOOLS_DIR = getToolsDir(); interface ToolConfig { name: string; diff --git a/packages/coding-agent/src/tui/model-selector.ts b/packages/coding-agent/src/tui/model-selector.ts index e16e64f7..d84aeb79 100644 --- a/packages/coding-agent/src/tui/model-selector.ts +++ b/packages/coding-agent/src/tui/model-selector.ts @@ -82,7 +82,7 @@ export class ModelSelectorComponent extends Container { } private async loadModels(): Promise { - // Load available models fresh (includes custom models from ~/.pi/agent/models.json) + // Load available models fresh (includes custom models from models.json) const { models: availableModels, error } = await getAvailableModels(); // If there's an error loading models.json, we'll show it via the "no models" path diff --git a/packages/coding-agent/src/tui/tui-renderer.ts b/packages/coding-agent/src/tui/tui-renderer.ts index 2f3d0ee3..2a4457d7 100644 --- a/packages/coding-agent/src/tui/tui-renderer.ts +++ b/packages/coding-agent/src/tui/tui-renderer.ts @@ -1,5 +1,4 @@ import * as fs from "node:fs"; -import * as os from "node:os"; import * as path from "node:path"; import type { Agent, AgentEvent, AgentState, ThinkingLevel } from "@mariozechner/pi-agent-core"; import type { AssistantMessage, Message, Model } from "@mariozechner/pi-ai"; @@ -19,6 +18,7 @@ import { } from "@mariozechner/pi-tui"; import { exec } from "child_process"; import { getChangelogPath, parseChangelog } from "../changelog.js"; +import { APP_NAME, getDebugLogPath, getModelsPath, getOAuthPath } from "../config.js"; import { exportSessionToHtml } from "../export-html.js"; import { getApiKeyForModel, getAvailableModels, invalidateOAuthCache } from "../model-config.js"; import { listOAuthProviders, login, logout } from "../oauth/index.js"; @@ -221,7 +221,7 @@ export class TuiRenderer { if (this.isInitialized) return; // Add header with logo and instructions - const logo = theme.bold(theme.fg("accent", "pi")) + theme.fg("dim", ` v${this.version}`); + const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`); const instructions = theme.fg("dim", "esc") + theme.fg("muted", " to interrupt") + @@ -434,7 +434,7 @@ export class TuiRenderer { this.showError( "No model selected.\n\n" + "Set an API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)\n" + - "or create ~/.pi/agent/models.json\n\n" + + `or create ${getModelsPath()}\n\n` + "Then use /model to select a model.", ); return; @@ -445,7 +445,7 @@ export class TuiRenderer { if (!apiKey) { this.showError( `No API key found for ${currentModel.provider}.\n\n` + - `Set the appropriate environment variable or update ~/.pi/agent/models.json`, + `Set the appropriate environment variable or update ${getModelsPath()}`, ); this.editor.setText(text); return; @@ -1334,9 +1334,7 @@ export class TuiRenderer { this.chatContainer.addChild( new Text(theme.fg("success", `✓ Successfully logged in to ${providerId}`), 1, 0), ); - this.chatContainer.addChild( - new Text(theme.fg("dim", `Tokens saved to ~/.pi/agent/oauth.json`), 1, 0), - ); + this.chatContainer.addChild(new Text(theme.fg("dim", `Tokens saved to ${getOAuthPath()}`), 1, 0)); this.ui.requestRender(); } catch (error: any) { this.showError(`Login failed: ${error.message}`); @@ -1353,7 +1351,7 @@ export class TuiRenderer { new Text(theme.fg("success", `✓ Successfully logged out of ${providerId}`), 1, 0), ); this.chatContainer.addChild( - new Text(theme.fg("dim", `Credentials removed from ~/.pi/agent/oauth.json`), 1, 0), + new Text(theme.fg("dim", `Credentials removed from ${getOAuthPath()}`), 1, 0), ); this.ui.requestRender(); } catch (error: any) { @@ -1545,7 +1543,7 @@ export class TuiRenderer { const width = (this.ui as any).terminal.columns; const allLines = this.ui.render(width); - const debugLogPath = path.join(os.homedir(), ".pi", "agent", "pi-debug.log"); + const debugLogPath = getDebugLogPath(); const debugData = [ `Debug output at ${new Date().toISOString()}`, `Terminal width: ${width}`, @@ -1566,11 +1564,7 @@ export class TuiRenderer { // Show confirmation this.chatContainer.addChild(new Spacer(1)); this.chatContainer.addChild( - new Text( - theme.fg("accent", "✓ Debug log written") + "\n" + theme.fg("muted", `~/.pi/agent/pi-debug.log`), - 1, - 1, - ), + new Text(theme.fg("accent", "✓ Debug log written") + "\n" + theme.fg("muted", debugLogPath), 1, 1), ); this.ui.requestRender();