feat(coding-agent): configurable app name and config dir for forks (#95)

- Add piConfig to package.json for app name and config directory
- Consolidate paths.ts into config.ts with clearer naming
- Fix Bun binary detection (changed from %7EBUN to $bunfs)
- Update all hardcoded paths to use config.ts exports
- getThemesDir() for built-in themes, getCustomThemesDir() for user themes
This commit is contained in:
Mario Zechner 2025-12-03 16:18:59 +01:00
parent 9d5fe1fe85
commit e82fb0fc83
17 changed files with 241 additions and 167 deletions

View file

@ -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

View file

@ -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:

View file

@ -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"
},

View file

@ -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";

View file

@ -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`);
}

View file

@ -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)
*/

View file

@ -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<KnownProvider, string> = {
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 <name> 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;

View file

@ -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<Api>[]; error: string | null } {
const configPath = join(homedir(), ".pi", "agent", "models.json");
const configPath = getModelsPath();
if (!existsSync(configPath)) {
return { models: [], error: null };
}

View file

@ -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);

View file

@ -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"));
}

View file

@ -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 });

View file

@ -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();
}

View file

@ -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;

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 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<string, ThemeJson> | undefined;
function getBuiltinThemes(): Record<string, ThemeJson> {
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<string, ThemeJson> {
return BUILTIN_THEMES;
}
function getThemesDir(): string {
return path.join(os.homedir(), ".pi", "agent", "themes");
}
export function getAvailableThemes(): string[] {
const themes = new Set<string>(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)) {

View file

@ -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;

View file

@ -82,7 +82,7 @@ export class ModelSelectorComponent extends Container {
}
private async loadModels(): Promise<void> {
// 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

View file

@ -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();