diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 119f7fe4d..0a883da21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -166,7 +166,7 @@ jobs: run: | mkdir -p packages/coding-agent/binaries ENTRYPOINT="./packages/coding-agent/src/cli.ts" - DEFINES="--define OMP_COMPILED=true --root ." + DEFINES="--define PI_COMPILED=true --root ." build_binary() { PLATFORM="$1" ARCH="$2" diff --git a/README.md b/README.md index 644a5f579..19d29e12a 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,7 @@ Configure different models for different purposes with automatic discovery: - **Three roles**: `default` (main model), `smol` (fast/cheap), `slow` (comprehensive reasoning) - **Auto-discovery**: Smol finds haiku → flash → mini; Slow finds codex → gpt → opus → pro - **Role-based selection**: Task tool agents can use `model: pi/smol` for cost-effective exploration -- CLI args (`--smol`, `--slow`) and env vars (`OMP_SMOL_MODEL`, `OMP_SLOW_MODEL`) +- CLI args (`--smol`, `--slow`) and env vars (`PI_SMOL_MODEL`, `PI_SLOW_MODEL`) - Configure via `/model` selector with keybindings (Enter=default, S=smol, L=slow) ## + Todo Tool (Task Tracking) diff --git a/bun.lock b/bun.lock index 388394dee..642a762e7 100644 --- a/bun.lock +++ b/bun.lock @@ -154,6 +154,7 @@ "version": "10.6.2", "dependencies": { "@oh-my-pi/pi-natives": "workspace:*", + "@oh-my-pi/pi-utils": "workspace:*", "@types/mime-types": "^3.0.1", "chalk": "^5.6.2", "marked": "^17.0.1", diff --git a/docs/porting-to-natives.md b/docs/porting-to-natives.md index 01e213584..b8db052f1 100644 --- a/docs/porting-to-natives.md +++ b/docs/porting-to-natives.md @@ -17,11 +17,13 @@ Avoid ports that depend on JS-only state or dynamic imports. N-API exports shoul ## Anatomy of a native export **Rust side:** + - Implementation lives in `crates/pi-natives/src/.rs`. If you add a new module, register it in `crates/pi-natives/src/lib.rs`. - Export with `#[napi]` and `#[napi(js_name = "...")]` to keep JS-facing camelCase names. Use `#[napi(object)]` for structs. - Use `task::blocking(tag, cancel_token, work)` (see `crates/pi-natives/src/task.rs`) for CPU-bound or blocking work. Use `task::future(env, tag, work)` for async work that needs Tokio (e.g., shell sessions). Pass a `CancelToken` when you expose `timeoutMs` or `AbortSignal`. **JS side:** + - `packages/natives/src/bindings.ts` holds the base `NativeBindings` interface. - `packages/natives/src//types.ts` defines TS types and augments `NativeBindings` via declaration merging. - `packages/natives/src/native.ts` imports each `/types.ts` file to activate the declarations. @@ -31,43 +33,51 @@ Avoid ports that depend on JS-only state or dynamic imports. N-API exports shoul ## Porting checklist -1) **Add the Rust implementation** +1. **Add the Rust implementation** + - Put the core logic in a plain Rust function. - If it’s a new module, add it to `crates/pi-natives/src/lib.rs`. - Expose it with `#[napi(js_name = "...")]` to keep camelCase names stable. - Keep signatures owned and simple: `String`, `Vec`, `Uint8Array`, or `Either` for large string/byte inputs. - For CPU-bound or blocking work, use `task::blocking`; for async work, use `task::future`. Pass a `CancelToken` and call `heartbeat()` inside long loops. -2) **Wire JS bindings** +2. **Wire JS bindings** + - Add the types and `NativeBindings` augmentation in `packages/natives/src//types.ts`. - Import `.//types` in `packages/natives/src/native.ts` to trigger declaration merging. - Add a wrapper in `packages/natives/src//index.ts` that calls `native`. - Re-export from `packages/natives/src/index.ts`. -3) **Update native validation** +3. **Update native validation** + - Add `checkFn("newExport")` in `validateNative` (`packages/natives/src/native.ts`). -4) **Add benchmarks** +4. **Add benchmarks** + - Put benchmarks next to the owning package (`packages/tui/bench`, `packages/natives/bench`, or `packages/coding-agent/bench`). - Include a JS baseline and native version in the same run. - Use `performance.now()` and a fixed iteration count. - Keep the benchmark inputs small and realistic (actual data seen in the hot path). -5) **Build the native binary** -- `bun --cwd=packages/natives run build:native` -- Use `bun --cwd=packages/natives run dev:native` for debug builds (`pi_natives.dev.node`) and set `OMP_DEV=1` when loading it. +5. **Build the native binary** + +- `bun --cwd=packages/natives run build:native` +- Use `bun --cwd=packages/natives run dev:native` for debug builds (`pi_natives.dev.node`) and set `PI_DEV=1` when loading it. + +6. **Run the benchmark** -6) **Run the benchmark** - `bun run packages//bench/.ts` (or `bun --cwd=packages/natives run bench`) -7) **Decide on usage** +7. **Decide on usage** + - If native is slower, **keep JS** and leave the native export unused. - If native is faster, switch call sites to the native wrapper. ## Pain points and how to avoid them ### 1) Stale `pi_natives.node` prevents new exports -The loader prefers the platform-tagged binary in `packages/natives/native` (`pi_natives.-.node`). When `OMP_DEV=1`, it will load `pi_natives.dev.node` instead. There is also a fallback `pi_natives.node`. Compiled binaries extract to `~/.omp/natives//pi_natives.-.node`. If any of these are stale, exports won’t update. + +The loader prefers the platform-tagged binary in `packages/natives/native` (`pi_natives.-.node`). When `PI_DEV=1`, it will load `pi_natives.dev.node` instead. There is also a fallback `pi_natives.node`. Compiled binaries extract to `~/.omp/natives//pi_natives.-.node`. If any of these are stale, exports won’t update. **Fix:** remove the stale file before rebuilding. @@ -90,6 +100,7 @@ bun -e 'const tag = `${process.platform}-${process.arch}`; const mod = require(` ``` ### 2) “Missing exports” errors from `validateNative` + This is **good** — it prevents silent mismatches. When you see this: ``` @@ -99,9 +110,11 @@ Native addon missing exports ... Missing: visibleWidth it means your binary is stale, the Rust `#[napi(js_name = "...")]` doesn’t match the JS name, or the export never compiled in. Fix the build and the naming mismatch, don’t weaken validation. ### 3) Rust signature mismatch + Keep it simple and owned. `String`, `Vec`, and `Uint8Array` work. Avoid references like `&str` in public exports. If you need structured data, wrap it in `#[napi(object)]` structs. ### 4) Benchmarking mistakes + - Don’t compare different inputs or allocations. - Keep JS and native using identical input arrays. - Run both in the same benchmark file to avoid skew. diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 62797b52e..da880136a 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,14 @@ # Changelog ## [Unreleased] +### Changed + +- Replaced direct `process.env` access with `getEnv()` utility from `@oh-my-pi/pi-utils` for consistent environment variable handling across all providers +- Updated environment variable names from `OMP_*` prefix to `PI_*` prefix for consistency (e.g., `OMP_CODING_AGENT_DIR` → `PI_CODING_AGENT_DIR`) + +### Removed + +- Removed automatic environment variable migration from `PI_*` to `OMP_*` prefixes via `migrate-env.ts` module ## [10.5.0] - 2026-02-04 ### Changed diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 852fd3301..12e61b562 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -1,6 +1,7 @@ #!/usr/bin/env bun import { join } from "node:path"; +import { getEnv } from "@oh-my-pi/pi-utils"; import type { Api, KnownProvider, Model } from "../src/types"; const packageRoot = join(import.meta.dir, ".."); @@ -191,7 +192,6 @@ interface KimiModelInfo { async function fetchKimiCodeModels(): Promise[]> { // Kimi Code /models endpoint requires authentication // Use KIMI_API_KEY env var if available, otherwise return fallback models - const apiKey = process.env.KIMI_API_KEY; if (apiKey) { try { diff --git a/packages/ai/src/cli.ts b/packages/ai/src/cli.ts index 4e4762bb8..e7e24da56 100755 --- a/packages/ai/src/cli.ts +++ b/packages/ai/src/cli.ts @@ -1,7 +1,6 @@ #!/usr/bin/env bun import { createInterface } from "readline"; import { CliAuthStorage } from "./storage"; -import "./utils/migrate-env"; import { getOAuthProviders } from "./utils/oauth"; import { loginAnthropic } from "./utils/oauth/anthropic"; import { loginCursor } from "./utils/oauth/cursor"; diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index d0824ac9e..8ed79e8de 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -1,5 +1,3 @@ -import "./utils/migrate-env"; - export * from "./models"; export * from "./providers/anthropic"; export * from "./providers/azure-openai-responses"; diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index 146a4ce99..7775900e7 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -18,6 +18,7 @@ import { type ToolConfiguration, ToolResultStatus, } from "@aws-sdk/client-bedrock-runtime"; +import { getEnv } from "@oh-my-pi/pi-utils"; import { calculateCost } from "../models"; import type { Api, @@ -97,7 +98,7 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = ( // in Node.js/Bun environment only if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) { - config.region = config.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION; + config.region = config.region || getEnv("AWS_REGION") || getEnv("AWS_DEFAULT_REGION"); } config.region = config.region || "us-east-1"; diff --git a/packages/ai/src/providers/cursor.ts b/packages/ai/src/providers/cursor.ts index cb5c911b6..b58c9a9b7 100644 --- a/packages/ai/src/providers/cursor.ts +++ b/packages/ai/src/providers/cursor.ts @@ -3,6 +3,7 @@ import * as fs from "node:fs/promises"; import http2 from "node:http2"; import { create, fromBinary, fromJson, type JsonValue, toBinary, toJson } from "@bufbuild/protobuf"; import { ValueSchema } from "@bufbuild/protobuf/wkt"; +import { getEnv } from "@oh-my-pi/pi-utils"; import { calculateCost } from "../models"; import type { Api, @@ -138,7 +139,7 @@ interface CursorLogEntry { } async function appendCursorDebugLog(entry: CursorLogEntry): Promise { - const logPath = process.env.DEBUG_CURSOR_LOG; + const logPath = getEnv("DEBUG_CURSOR_LOG"); if (!logPath) return; try { await fs.appendFile(logPath, `${JSON.stringify(entry, debugReplacer)}\n`); @@ -148,10 +149,10 @@ async function appendCursorDebugLog(entry: CursorLogEntry): Promise { } function log(type: string, subtype?: string, data?: unknown): void { - if (!process.env.DEBUG_CURSOR) return; + if (!getEnv("DEBUG_CURSOR")) return; const normalizedData = data ? decodeLogData(data) : data; const entry: CursorLogEntry = { ts: Date.now(), type, subtype, data: normalizedData }; - const verbose = process.env.DEBUG_CURSOR === "2" || process.env.DEBUG_CURSOR === "verbose"; + const verbose = getEnv("DEBUG_CURSOR") === "2" || getEnv("DEBUG_CURSOR") === "verbose"; const dataStr = verbose && normalizedData ? ` ${JSON.stringify(normalizedData, debugReplacer)?.slice(0, 500)}` : ""; console.error(`[CURSOR] ${type}${subtype ? `: ${subtype}` : ""}${dataStr}`); void appendCursorDebugLog(entry); @@ -2065,7 +2066,7 @@ function buildGrpcRequest( const toolNames = context.tools?.map(tool => tool.name) ?? []; const detail = - process.env.DEBUG_CURSOR === "2" + getEnv("DEBUG_CURSOR") === "2" ? ` ${JSON.stringify(clientMessage.message.value, debugReplacer, 2)?.slice(0, 2000)}` : ""; log("info", "builtRunRequest", { diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/providers/google-vertex.ts index 2574c413a..c7b2cb736 100644 --- a/packages/ai/src/providers/google-vertex.ts +++ b/packages/ai/src/providers/google-vertex.ts @@ -6,6 +6,7 @@ import { ThinkingLevel, } from "@google/genai"; import { calculateCost } from "../models"; +import { getEnv } from "@oh-my-pi/pi-utils"; import type { Api, AssistantMessage, @@ -304,7 +305,7 @@ function createClient(model: Model<"google-vertex">, project: string, location: } function resolveProject(options?: GoogleVertexOptions): string { - const project = options?.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT; + const project = options?.project || getEnv("GOOGLE_CLOUD_PROJECT") || getEnv("GCLOUD_PROJECT"); if (!project) { throw new Error( "Vertex AI requires a project ID. Set GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT or pass project in options.", @@ -314,7 +315,7 @@ function resolveProject(options?: GoogleVertexOptions): string { } function resolveLocation(options?: GoogleVertexOptions): string { - const location = options?.location || process.env.GOOGLE_CLOUD_LOCATION; + const location = options?.location || getEnv("GOOGLE_CLOUD_LOCATION"); if (!location) { throw new Error("Vertex AI requires a location. Set GOOGLE_CLOUD_LOCATION or pass location in options."); } diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index 0cbfb990e..46d650cc4 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -1,5 +1,5 @@ import * as os from "node:os"; -import { abortableSleep } from "@oh-my-pi/pi-utils"; +import { abortableSleep, getEnv } from "@oh-my-pi/pi-utils"; import type { ResponseFunctionToolCall, ResponseInput, @@ -71,7 +71,7 @@ export function buildCodexSystemPrompt(args: { userSystemPrompt?: string }): Cod }; } -const CODEX_DEBUG = process.env.OMP_CODEX_DEBUG === "1" || process.env.OMP_CODEX_DEBUG === "true"; +const CODEX_DEBUG = getEnv("PI_CODEX_DEBUG") === "1" || getEnv("PI_CODEX_DEBUG") === "true"; const CODEX_MAX_RETRIES = 2; const CODEX_RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]); const CODEX_RETRY_DELAY_MS = 500; diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 0b3433fea..6053214d6 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -10,6 +10,7 @@ import type { } from "openai/resources/chat/completions"; import { calculateCost } from "../models"; import { getEnvApiKey } from "../stream"; +import { getEnv } from "@oh-my-pi/pi-utils"; import type { AssistantMessage, Context, @@ -348,12 +349,12 @@ async function createClient( extraHeaders?: Record, ) { if (!apiKey) { - if (!process.env.OPENAI_API_KEY) { + if (!getEnv("OPENAI_API_KEY")) { throw new Error( "OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass it as an argument.", ); } - apiKey = process.env.OPENAI_API_KEY; + apiKey = getEnv("OPENAI_API_KEY"); } let headers = { ...(model.headers ?? {}), ...(extraHeaders ?? {}) }; diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index 3e7a93649..c816bcc0a 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -357,12 +357,12 @@ function createClient( extraHeaders?: Record, ) { if (!apiKey) { - if (!process.env.OPENAI_API_KEY) { + if (!getEnv("OPENAI_API_KEY")) { throw new Error( "OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass it as an argument.", ); } - apiKey = process.env.OPENAI_API_KEY; + apiKey = getEnv("OPENAI_API_KEY"); } const headers = { ...(model.headers ?? {}), ...(extraHeaders ?? {}) }; diff --git a/packages/ai/src/storage.ts b/packages/ai/src/storage.ts index 911784102..101e62075 100644 --- a/packages/ai/src/storage.ts +++ b/packages/ai/src/storage.ts @@ -7,6 +7,7 @@ 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 { getEnv } from "@oh-my-pi/pi-utils"; import type { OAuthCredentials } from "./utils/oauth/types"; type AuthCredential = { type: "api_key"; key: string } | ({ type: "oauth" } & OAuthCredentials); @@ -24,7 +25,7 @@ type AuthRow = { * Get the agent config directory (e.g., ~/.omp/agent/) */ function getAgentDir(): string { - const configDir = process.env.OMP_CODING_AGENT_DIR || path.join(os.homedir(), ".omp", "agent"); + const configDir = getEnv("PI_CODING_AGENT_DIR") || path.join(os.homedir(), ".omp", "agent"); return configDir; } diff --git a/packages/ai/src/usage/kimi.ts b/packages/ai/src/usage/kimi.ts index b2d30757b..1f5a52a7e 100644 --- a/packages/ai/src/usage/kimi.ts +++ b/packages/ai/src/usage/kimi.ts @@ -10,6 +10,7 @@ import type { UsageWindow, } from "../usage"; import { getKimiCommonHeaders, refreshKimiToken } from "../utils/oauth/kimi"; +import { getEnv } from "@oh-my-pi/pi-utils"; const DEFAULT_BASE_URL = "https://api.kimi.com/coding/v1"; const USAGE_PATH = "usages"; @@ -45,7 +46,7 @@ const toNumber = (value: unknown): number | undefined => { }; function normalizeBaseUrl(baseUrl?: string): string { - const envBase = process.env.KIMI_CODE_BASE_URL?.trim(); + const envBase = getEnv("KIMI_CODE_BASE_URL")?.trim(); const candidate = baseUrl?.trim() || envBase || DEFAULT_BASE_URL; return candidate.replace(/\/+$/, ""); } diff --git a/packages/ai/src/utils/migrate-env.ts b/packages/ai/src/utils/migrate-env.ts deleted file mode 100644 index 367e79494..000000000 --- a/packages/ai/src/utils/migrate-env.ts +++ /dev/null @@ -1,8 +0,0 @@ -for (const [key, value] of Object.entries(process.env)) { - if (key.startsWith("PI_") && value !== undefined) { - const ompKey = `OMP_${key.slice(3)}`; // PI_FOO -> OMP_FOO - if (process.env[ompKey] === undefined) { - process.env[ompKey] = value; - } - } -} diff --git a/packages/ai/src/utils/oauth/google-gemini-cli.ts b/packages/ai/src/utils/oauth/google-gemini-cli.ts index bbbe90eb0..4c099de79 100644 --- a/packages/ai/src/utils/oauth/google-gemini-cli.ts +++ b/packages/ai/src/utils/oauth/google-gemini-cli.ts @@ -5,6 +5,7 @@ import { OAuthCallbackFlow } from "./callback-server"; import { generatePKCE } from "./pkce"; import type { OAuthController, OAuthCredentials } from "./types"; +import { getEnv } from "@oh-my-pi/pi-utils"; const decode = (s: string) => atob(s); const CLIENT_ID = decode( @@ -91,7 +92,7 @@ async function pollOperation( } async function discoverProject(accessToken: string, onProgress?: (message: string) => void): Promise { - const envProjectId = process.env.GOOGLE_CLOUD_PROJECT || process.env.GOOGLE_CLOUD_PROJECT_ID; + const envProjectId = getEnv("GOOGLE_CLOUD_PROJECT") || getEnv("GOOGLE_CLOUD_PROJECT_ID"); const headers = { Authorization: `Bearer ${accessToken}`, diff --git a/packages/ai/src/utils/oauth/kimi.ts b/packages/ai/src/utils/oauth/kimi.ts index 1881dbca0..8135df97c 100644 --- a/packages/ai/src/utils/oauth/kimi.ts +++ b/packages/ai/src/utils/oauth/kimi.ts @@ -6,7 +6,7 @@ import * as crypto from "node:crypto"; import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; -import { abortableSleep, isEnoent } from "@oh-my-pi/pi-utils"; +import { abortableSleep, getEnv, isEnoent } from "@oh-my-pi/pi-utils"; import packageJson from "../../../package.json" with { type: "json" }; import type { OAuthController, OAuthCredentials } from "./types"; @@ -37,12 +37,12 @@ interface TokenResponse { } function getAgentDir(): string { - const configDir = process.env.OMP_CODING_AGENT_DIR || path.join(os.homedir(), ".omp", "agent"); + const configDir = getEnv("PI_CODING_AGENT_DIR") || path.join(os.homedir(), ".omp", "agent"); return configDir; } function resolveOAuthHost(): string { - return process.env.KIMI_CODE_OAUTH_HOST || process.env.KIMI_OAUTH_HOST || DEFAULT_OAUTH_HOST; + return getEnv("KIMI_CODE_OAUTH_HOST") || getEnv("KIMI_OAUTH_HOST") || DEFAULT_OAUTH_HOST; } function formatDeviceModel(system: string, release: string, arch: string): string { diff --git a/packages/ai/test/context-overflow.test.ts b/packages/ai/test/context-overflow.test.ts index 12b242371..bdf20a1cc 100644 --- a/packages/ai/test/context-overflow.test.ts +++ b/packages/ai/test/context-overflow.test.ts @@ -447,7 +447,7 @@ describe("Context overflow error handling", () => { // Check if ollama is installed and local LLM tests are enabled let ollamaInstalled = false; - if (!process.env.OMP_NO_LOCAL_LLM) { + if (!process.env.PI_NO_LOCAL_LLM) { try { execSync("which ollama", { stdio: "ignore" }); ollamaInstalled = true; @@ -541,7 +541,7 @@ describe("Context overflow error handling", () => { // ============================================================================= let lmStudioRunning = false; - if (!process.env.OMP_NO_LOCAL_LLM) { + if (!process.env.PI_NO_LOCAL_LLM) { try { execSync("curl -s --max-time 1 http://localhost:1234/v1/models > /dev/null", { stdio: "ignore" }); lmStudioRunning = true; diff --git a/packages/ai/test/openai-codex-stream.test.ts b/packages/ai/test/openai-codex-stream.test.ts index 35d4e9693..f8dd88f53 100644 --- a/packages/ai/test/openai-codex-stream.test.ts +++ b/packages/ai/test/openai-codex-stream.test.ts @@ -4,14 +4,14 @@ import type { Context, Model } from "@oh-my-pi/pi-ai/types"; import { TempDir } from "@oh-my-pi/pi-utils"; const originalFetch = global.fetch; -const originalAgentDir = process.env.OMP_CODING_AGENT_DIR; +const originalAgentDir = process.env.PI_CODING_AGENT_DIR; afterEach(() => { global.fetch = originalFetch; if (originalAgentDir === undefined) { - delete process.env.OMP_CODING_AGENT_DIR; + delete process.env.PI_CODING_AGENT_DIR; } else { - process.env.OMP_CODING_AGENT_DIR = originalAgentDir; + process.env.PI_CODING_AGENT_DIR = originalAgentDir; } vi.restoreAllMocks(); }); @@ -19,7 +19,7 @@ afterEach(() => { describe("openai-codex streaming", () => { it("streams SSE responses into AssistantMessageEventStream", async () => { const tempDir = TempDir.createSync("@pi-codex-stream-"); - process.env.OMP_CODING_AGENT_DIR = tempDir.path(); + process.env.PI_CODING_AGENT_DIR = tempDir.path(); const payload = Buffer.from( JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }), @@ -130,7 +130,7 @@ describe("openai-codex streaming", () => { it("sets conversation_id/session_id headers and prompt_cache_key when sessionId is provided", async () => { const tempDir = TempDir.createSync("@pi-codex-stream-"); - process.env.OMP_CODING_AGENT_DIR = tempDir.path(); + process.env.PI_CODING_AGENT_DIR = tempDir.path(); const payload = Buffer.from( JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }), @@ -231,7 +231,7 @@ describe("openai-codex streaming", () => { it("does not set conversation_id/session_id headers when sessionId is not provided", async () => { const tempDir = TempDir.createSync("@pi-codex-stream-"); - process.env.OMP_CODING_AGENT_DIR = tempDir.path(); + process.env.PI_CODING_AGENT_DIR = tempDir.path(); const payload = Buffer.from( JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }), diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index eeba6d634..b7cca2bf0 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -1350,7 +1350,7 @@ describe("Generate E2E Tests", () => { // Check if ollama is installed and local LLM tests are enabled let ollamaInstalled = false; - if (!process.env.OMP_NO_LOCAL_LLM) { + if (!process.env.PI_NO_LOCAL_LLM) { try { execSync("which ollama", { stdio: "ignore" }); ollamaInstalled = true; diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index bc1fc4451..874557fad 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] + ### Added - Added `task.maxRecursionDepth` setting to control how many levels deep subagents can spawn their own subagents (0=none, 1=one level, 2=two levels, -1=unlimited) @@ -12,6 +13,9 @@ ### Changed +- Renamed environment variable prefix from `OMP_` to `PI_` throughout codebase (e.g., `OMP_DEBUG_STARTUP` → `PI_DEBUG_STARTUP`, `OMP_PYTHON_GATEWAY_URL` → `PI_PYTHON_GATEWAY_URL`) +- Removed `env` setting from configuration schema; environment variables are no longer automatically applied from settings +- Changed `venvPath` property in PythonRuntime from nullable to optional (returns `undefined` instead of `null`) - Simplified notification settings from protocol-specific options (bell, osc99, osc9) to simple on/off toggle for `completion.notify` and `ask.notify` - Moved notification protocol detection and sending to `TERMINAL` API from local utility functions - Changed task tool spawns configuration from "explore" to "*" to allow subagents to spawn any agent type diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 62b0c2c76..ad1047066 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -59,13 +59,13 @@ npm install -g @oh-my-pi/pi-coding-agent Download from [GitHub Releases](https://github.com/can1357/oh-my-pi/releases): -| Platform | Binary | -| ------------------- | ------------------------ | -| macOS Apple Silicon | `omp-darwin-arm64` | -| macOS Intel | `omp-darwin-x64` | -| Linux x64 | `omp-linux-x64` | -| Linux ARM64 | `omp-linux-arm64` | -| Windows x64 | `omp-windows-x64.exe` | +| Platform | Binary | +| ------------------- | --------------------- | +| macOS Apple Silicon | `omp-darwin-arm64` | +| macOS Intel | `omp-darwin-x64` | +| Linux x64 | `omp-linux-x64` | +| Linux ARM64 | `omp-linux-arm64` | +| Windows x64 | `omp-windows-x64.exe` | ```bash # macOS/Linux @@ -201,24 +201,24 @@ The agent reads, writes, and edits files, and executes commands via bash. ### Slash Commands -| Command | Description | -| ------------------------- | --------------------------------------------------------------------------- | -| `/settings` | Open settings menu (thinking, theme, queue mode, toggles) | +| Command | Description | +| ------------------------- | ----------------------------------------------------------------------------------------------- | +| `/settings` | Open settings menu (thinking, theme, queue mode, toggles) | | `/model` | Switch models mid-session. Use `/model ` or `provider/model` to prefilter/disambiguate. | -| `/export [file]` | Export session to HTML file | -| `/dump` | Copy session transcript to clipboard | -| `/share` | Upload session as secret GitHub gist, get shareable URL (requires `gh` CLI) | -| `/session` | Show session info: path, message counts, token usage, cost | -| `/hotkeys` | Show all keyboard shortcuts | -| `/changelog` | Display full version history | -| `/tree` | Navigate session tree in-place (search, filter, label entries) | -| `/branch` | Create new conversation branch from a previous message | -| `/resume` | Switch to a different session (interactive selector) | -| `/login` | OAuth login for subscription-based models | -| `/logout` | Clear OAuth tokens | -| `/new` | Start a new session | -| `/copy` | Copy last agent message to clipboard | -| `/compact [instructions]` | Manually compact conversation context | +| `/export [file]` | Export session to HTML file | +| `/dump` | Copy session transcript to clipboard | +| `/share` | Upload session as secret GitHub gist, get shareable URL (requires `gh` CLI) | +| `/session` | Show session info: path, message counts, token usage, cost | +| `/hotkeys` | Show all keyboard shortcuts | +| `/changelog` | Display full version history | +| `/tree` | Navigate session tree in-place (search, filter, label entries) | +| `/branch` | Create new conversation branch from a previous message | +| `/resume` | Switch to a different session (interactive selector) | +| `/login` | OAuth login for subscription-based models | +| `/logout` | Clear OAuth tokens | +| `/new` | Start a new session | +| `/copy` | Copy last agent message to clipboard | +| `/compact [instructions]` | Manually compact conversation context | ### Editor Features @@ -245,13 +245,13 @@ The agent reads, writes, and edits files, and executes commands via bash. **Editing:** -| Key | Action | -| ------------------------- | ---------------------------- | -| Enter | Send message | +| Key | Action | +| ------------------------- | ----------------------------------------- | +| Enter | Send message | | Shift+Enter | New line (Ctrl+Enter on Windows Terminal) | -| Ctrl+W / Option+Backspace | Delete word backwards | -| Ctrl+U | Delete to start of line | -| Ctrl+K | Delete to end of line | +| Ctrl+W / Option+Backspace | Delete word backwards | +| Ctrl+U | Delete to start of line | +| Ctrl+K | Delete to end of line | **Other:** @@ -366,6 +366,7 @@ When disabled, neither case triggers automatic compaction (use `/compact` manual ``` **Environment Variables (`env`):** + - Automatically sets environment variables when the application starts - Only sets variables that aren't already present in `process.env` - Supports any environment variable, not just API keys @@ -564,7 +565,7 @@ Global `~/.omp/agent/settings.json` stores persistent preferences: | Setting | Description | Default | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | --------------- | | `theme` | Color theme name | auto-detected | -| `modelRoles` | Model assignments by role (e.g., `{"default": "...", "slow": "...", "smol": "...", "plan": "..."}`) | - | +| `modelRoles` | Model assignments by role (e.g., `{"default": "...", "slow": "...", "smol": "...", "plan": "..."}`) | - | | `defaultThinkingLevel` | Thinking level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh` | - | | `enabledModels` | Model patterns for cycling. Supports glob patterns (`github-copilot/*`, `*sonnet*`) and fuzzy matching. Same as `--models` CLI flag | - | | `queueMode` | Message queue mode: `all` or `one-at-a-time` | `one-at-a-time` | @@ -824,33 +825,33 @@ omp [options] [@files...] [messages...] ### Options -| Option | Description | -| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Option | Description | +| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `--provider ` | Provider: `anthropic`, `openai`, `google`, `mistral`, `xai`, `groq`, `cerebras`, `openrouter`, `zai`, `cursor`, `github-copilot`, `openai-codex`, `google-gemini-cli`, `google-antigravity`, or custom | -| `--model ` | Model ID | -| `--api-key ` | API key (overrides environment) | -| `--system-prompt ` | Custom system prompt (text or file path) | -| `--append-system-prompt ` | Append to system prompt | -| `--mode ` | Output mode: `text`, `json`, `rpc` (implies `--print`) | -| `--print`, `-p` | Non-interactive: process prompt and exit | -| `--no-session` | Don't save session | -| `--session ` | Use specific session file | -| `--session-dir ` | Directory for session storage and lookup | -| `--continue`, `-c` | Continue most recent session | -| `--resume`, `-r` | Select session to resume | -| `--models ` | Comma-separated patterns for model role cycling. Supports glob patterns (e.g., `anthropic/*`, `*sonnet*:high`) and fuzzy matching (e.g., `sonnet,haiku:low`) | -| `--no-tools` | Disable all built-in tools | -| `--tools ` | Restrict to comma-separated tool list (default: all tools enabled) | -| `--thinking ` | Thinking level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh` | -| `--extension `, `-e` | Load an extension file (can be used multiple times) | -| `--no-extensions` | Disable extension discovery (explicit `-e` paths still work) | -| `--no-skills` | Disable skills discovery and loading | -| `--skills ` | Comma-separated glob patterns to filter skills (e.g., `git-*,docker`) | -| `--no-lsp` | Disable LSP integration | -| `--hook ` | Load a hook file (for debugging) | -| `--export [output]` | Export session to HTML | -| `--help`, `-h` | Show help | -| `--version`, `-v` | Show version | +| `--model ` | Model ID | +| `--api-key ` | API key (overrides environment) | +| `--system-prompt ` | Custom system prompt (text or file path) | +| `--append-system-prompt ` | Append to system prompt | +| `--mode ` | Output mode: `text`, `json`, `rpc` (implies `--print`) | +| `--print`, `-p` | Non-interactive: process prompt and exit | +| `--no-session` | Don't save session | +| `--session ` | Use specific session file | +| `--session-dir ` | Directory for session storage and lookup | +| `--continue`, `-c` | Continue most recent session | +| `--resume`, `-r` | Select session to resume | +| `--models ` | Comma-separated patterns for model role cycling. Supports glob patterns (e.g., `anthropic/*`, `*sonnet*:high`) and fuzzy matching (e.g., `sonnet,haiku:low`) | +| `--no-tools` | Disable all built-in tools | +| `--tools ` | Restrict to comma-separated tool list (default: all tools enabled) | +| `--thinking ` | Thinking level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh` | +| `--extension `, `-e` | Load an extension file (can be used multiple times) | +| `--no-extensions` | Disable extension discovery (explicit `-e` paths still work) | +| `--no-skills` | Disable skills discovery and loading | +| `--skills ` | Comma-separated glob patterns to filter skills (e.g., `git-*,docker`) | +| `--no-lsp` | Disable LSP integration | +| `--hook ` | Load a hook file (for debugging) | +| `--export [output]` | Export session to HTML | +| `--help`, `-h` | Show help | +| `--version`, `-v` | Show version | ### File Arguments @@ -906,11 +907,11 @@ omp --export session.jsonl output.html ### Environment Variables -| Variable | Description | -|----------|-------------| +| Variable | Description | +| ------------------------------------------- | ----------------------------------------------------------------- | | `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc. | API keys for providers (see [API Keys & OAuth](#api-keys--oauth)) | -| `OMP_CODING_AGENT_DIR` | Override the agent config directory (default: `~/.omp/agent`) | -| `VISUAL`, `EDITOR` | External editor for Ctrl+G (e.g., `vim`, `code --wait`) | +| `PI_CODING_AGENT_DIR` | Override the agent config directory (default: `~/.omp/agent`) | +| `VISUAL`, `EDITOR` | External editor for Ctrl+G (e.g., `vim`, `code --wait`) | --- @@ -920,27 +921,27 @@ All tools are enabled by default. Use `--tools ` to restrict to a subset. ### Core Tools -| Tool | Description | -| ------- | --------------------------------------------------------------------------------------------------------------- | -| `read` | Read file contents. Images sent as attachments. Text: first 2000 lines. Use offset/limit for large files. | -| `write` | Write/overwrite file. Creates parent directories. | -| `edit` | Replace text in file with fuzzy whitespace matching. Fails if text appears multiple times or not found. | -| `bash` | Execute command. Returns stdout/stderr. Optional `timeout` parameter. | -| `grep` | Search file contents (regex or literal). Respects `.gitignore`. | -| `find` | Search for files by glob pattern. Respects `.gitignore`. | -| `ls` | List directory contents. Includes dotfiles. | +| Tool | Description | +| ------- | --------------------------------------------------------------------------------------------------------- | +| `read` | Read file contents. Images sent as attachments. Text: first 2000 lines. Use offset/limit for large files. | +| `write` | Write/overwrite file. Creates parent directories. | +| `edit` | Replace text in file with fuzzy whitespace matching. Fails if text appears multiple times or not found. | +| `bash` | Execute command. Returns stdout/stderr. Optional `timeout` parameter. | +| `grep` | Search file contents (regex or literal). Respects `.gitignore`. | +| `find` | Search for files by glob pattern. Respects `.gitignore`. | +| `ls` | List directory contents. Includes dotfiles. | ### Additional Built-in Tools -| Tool | Description | -| ------------- | ----------------------------------------------------- | -| `task` | Spawn sub-agents for complex multi-step tasks | -| `lsp` | Language Server Protocol queries (go-to-definition, references, hover) | -| `todo_write` | Track task progress during sessions | -| `web_search` | Search the web | -| `fetch` | Fetch and process URLs | -| `python` | Execute Python code in IPython kernel | -| `notebook` | Edit Jupyter notebook cells | +| Tool | Description | +| ------------ | ---------------------------------------------------------------------- | +| `task` | Spawn sub-agents for complex multi-step tasks | +| `lsp` | Language Server Protocol queries (go-to-definition, references, hover) | +| `todo_write` | Track task progress during sessions | +| `web_search` | Search the web | +| `fetch` | Fetch and process URLs | +| `python` | Execute Python code in IPython kernel | +| `notebook` | Edit Jupyter notebook cells | Example: `--tools read,grep,find,ls` for read-only code review. diff --git a/packages/coding-agent/docs/extension-loading.md b/packages/coding-agent/docs/extension-loading.md index 9dba40d8e..cba57bb24 100644 --- a/packages/coding-agent/docs/extension-loading.md +++ b/packages/coding-agent/docs/extension-loading.md @@ -23,7 +23,7 @@ For each root: Additional extension paths can be provided via settings and CLI: -- **Global settings**: `~/.omp/agent/config.yml` (or `$OMP_CODING_AGENT_DIR/config.yml`) +- **Global settings**: `~/.omp/agent/config.yml` (or `$PI_CODING_AGENT_DIR/config.yml`) - **Project settings**: `/.omp/settings.json` - **CLI**: `--extension` or `-e` @@ -39,7 +39,7 @@ extensions: ```json // .omp/settings.json { - "extensions": ["./project-extension.ts"] + "extensions": ["./project-extension.ts"] } ``` @@ -66,10 +66,10 @@ Example `package.json` manifest: ```json { - "name": "my-extension-pack", - "omp": { - "extensions": ["./src/safety-gates.ts", "./src/custom-tools.ts"] - } + "name": "my-extension-pack", + "omp": { + "extensions": ["./src/safety-gates.ts", "./src/custom-tools.ts"] + } } ``` diff --git a/packages/coding-agent/docs/python-repl.md b/packages/coding-agent/docs/python-repl.md index 64fb358f9..e1b02b2fd 100644 --- a/packages/coding-agent/docs/python-repl.md +++ b/packages/coding-agent/docs/python-repl.md @@ -6,6 +6,7 @@ - `jupyter-kernel-gateway` (`kernel_gateway` module) and `ipykernel` installed in the selected Python environment Install: + ```bash python -m pip install jupyter_kernel_gateway ipykernel ``` @@ -16,6 +17,7 @@ The Python tool uses a Jupyter Kernel Gateway and talks to it over REST and WebS By default it uses a shared local gateway so multiple pi instances reuse the same gateway process. Shared-gateway startup flow: + 1. Filter the environment and resolve the Python runtime (including venv detection) 2. Acquire the shared gateway (reuse a healthy gateway or spawn `python -m kernel_gateway` on 127.0.0.1:PORT) 3. Wait for gateway readiness (`GET /api/kernelspecs`) @@ -29,19 +31,21 @@ Instead of spawning a local gateway, you can connect to an already-running Jupyt ```bash # Connect to external gateway -export OMP_PYTHON_GATEWAY_URL="http://127.0.0.1:8888" +export PI_PYTHON_GATEWAY_URL="http://127.0.0.1:8888" # Optional: auth token if gateway requires it (KG_AUTH_TOKEN) -export OMP_PYTHON_GATEWAY_TOKEN="your-token-here" +export PI_PYTHON_GATEWAY_TOKEN="your-token-here" ``` -When `OMP_PYTHON_GATEWAY_URL` is set: +When `PI_PYTHON_GATEWAY_URL` is set: + - No local gateway process is spawned - Kernels are created on the external gateway - The gateway process is not killed on shutdown - Availability check uses `/api/kernelspecs` endpoint instead of local module check This is useful for: + - Remote kernel execution - Shared kernel environments - Pre-configured gateway setups @@ -49,7 +53,7 @@ This is useful for: ## Environment Propagation - The kernel inherits a filtered environment (explicit allowlist + denylist) -- Allowlisted prefixes include `LC_`, `XDG_`, and `OMP_`; known API-key vars are removed +- Allowlisted prefixes include `LC_`, `XDG_`, and `PI_`; known API-key vars are removed - `PYTHONPATH` is passed through if present - Virtual environments are detected via `VIRTUAL_ENV`, `.venv/`, or `venv/` and preferred when present @@ -65,18 +69,21 @@ Project modules override user modules with the same filename. ## Kernel Modes Settings under `python` control exposure and reuse: + - `toolMode`: `both` (default), `ipy-only`, `bash-only` - `kernelMode`: `session` (default) or `per-call` - `sharedGateway`: `true` (default). Setting to `false` throws an error because local (per-process) gateways are not supported; the shared gateway is required. Mode behavior: + - `session`: reuse kernels per session id, serialize execution, evict after 5 minutes of idle time (max 4 sessions) - `per-call`: create a fresh kernel per tool call and shut it down afterward Environment override: -- `OMP_PY=0|bash` → `bash-only` -- `OMP_PY=1|py` → `ipy-only` -- `OMP_PY=mix|both` → `both` + +- `PI_PY=0|bash` → `bash-only` +- `PI_PY=1|py` → `ipy-only` +- `PI_PY=mix|both` → `both` ## Shell Helper @@ -95,9 +102,9 @@ and returns a `ShellResult` with `stdout`, `stderr`, and `code`. ## Troubleshooting - **Kernel unavailable**: Ensure `python` + `jupyter-kernel-gateway` + `ipykernel` are installed; the session will fall back to bash-only. -- **Python mode override**: Check `python.toolMode` or `OMP_PY` if the Python tool is missing. +- **Python mode override**: Check `python.toolMode` or `PI_PY` if the Python tool is missing. - **Shared gateway disabled**: `python.sharedGateway=false` causes the Python tool to error because local (per-process) gateways are not supported. -- **Skip preflight checks**: Set `OMP_PYTHON_SKIP_CHECK=1` to bypass kernel availability checks. -- **External gateway unreachable**: Check the URL is correct and the gateway is running. If auth is required, set `OMP_PYTHON_GATEWAY_TOKEN`. -- **IPC tracing**: Set `OMP_PYTHON_IPC_TRACE=1` to log kernel message flow. +- **Skip preflight checks**: Set `PI_PYTHON_SKIP_CHECK=1` to bypass kernel availability checks. +- **External gateway unreachable**: Check the URL is correct and the gateway is running. If auth is required, set `PI_PYTHON_GATEWAY_TOKEN`. +- **IPC tracing**: Set `PI_PYTHON_IPC_TRACE=1` to log kernel message flow. - **Stdin requests**: Interactive input is not supported; refactor code to avoid `input()` or provide data programmatically. diff --git a/packages/coding-agent/docs/session.md b/packages/coding-agent/docs/session.md index 1acc95202..fdfdc0c27 100644 --- a/packages/coding-agent/docs/session.md +++ b/packages/coding-agent/docs/session.md @@ -8,7 +8,7 @@ Sessions are stored as JSONL (JSON Lines) files. Each line is a JSON object with ~/.omp/agent/sessions/----/_.jsonl ``` -Default base directory comes from `getAgentDir()` (overridable via `OMP_CODING_AGENT_DIR`). +Default base directory comes from `getAgentDir()` (overridable via `PI_CODING_AGENT_DIR`). `` is the working directory with the leading slash removed and `/`, `\`, `:` replaced by `-`. `` is ISO-8601 with `:`/`.` replaced by `-`. `sessionId` is a nanoid. @@ -49,7 +49,14 @@ interface SessionEntryBase { First line of the file. Metadata only, not part of the tree (no `id`/`parentId`). `version` is absent in v1 sessions. ```json -{ "type": "session", "version": 3, "id": "nanoid", "timestamp": "2024-12-03T14:00:00.000Z", "cwd": "/path/to/project", "title": "Optional title" } +{ + "type": "session", + "version": 3, + "id": "nanoid", + "timestamp": "2024-12-03T14:00:00.000Z", + "cwd": "/path/to/project", + "title": "Optional title" +} ``` For sessions with a parent (created via `/branch`, `newSession({ parentSession })`, or fork operations): @@ -280,7 +287,6 @@ const text = await Bun.file("session.jsonl").text(); const entries = Bun.JSONL.parse(text); for (const entry of entries) { - switch (entry.type) { case "session": console.log(`Session v${entry.version ?? 1}: ${entry.id}`); diff --git a/packages/coding-agent/docs/theme.md b/packages/coding-agent/docs/theme.md index 9f22caf72..c9f74ea8a 100644 --- a/packages/coding-agent/docs/theme.md +++ b/packages/coding-agent/docs/theme.md @@ -85,14 +85,14 @@ Used for native syntax highlighting in tool output and editors: Editor border colors that indicate the current thinking/reasoning level: -| Token | Purpose | -| ----------------- | ----------------------------------------------------------------- | -| `thinkingOff` | Border when thinking is off (most subtle) | -| `thinkingMinimal` | Border for minimal thinking | -| `thinkingLow` | Border for low thinking | -| `thinkingMedium` | Border for medium thinking | -| `thinkingHigh` | Border for high thinking | -| `thinkingXhigh` | Border for xhigh thinking (most prominent) | +| Token | Purpose | +| ----------------- | ------------------------------------------ | +| `thinkingOff` | Border when thinking is off (most subtle) | +| `thinkingMinimal` | Border for minimal thinking | +| `thinkingLow` | Border for low thinking | +| `thinkingMedium` | Border for medium thinking | +| `thinkingHigh` | Border for high thinking | +| `thinkingXhigh` | Border for xhigh thinking (most prominent) | These create a visual hierarchy: off → minimal → low → medium → high → xhigh @@ -101,26 +101,26 @@ These create a visual hierarchy: off → minimal → low → medium → high → | Token | Purpose | | ------------ | ------------------------------------------------ | | `bashMode` | Editor border color when in bash mode (! prefix) | -| `pythonMode` | Editor border color when in python mode (>>>) | +| `pythonMode` | Editor border color when in python mode (>>>) | ### Status Line (14 colors) -| Token | Purpose | -| ------------------- | --------------------------------------- | -| `statusLineBg` | Status line background | -| `statusLineSep` | Separators between status line segments | -| `statusLineModel` | Model segment text | -| `statusLinePath` | Working directory segment | -| `statusLineGitClean` | Git segment (clean) | -| `statusLineGitDirty` | Git segment (dirty) | -| `statusLineContext` | Context window usage segment | -| `statusLineSpend` | Token input/total segment | -| `statusLineStaged` | Git staged count | -| `statusLineDirty` | Git unstaged count | -| `statusLineUntracked` | Git untracked count | -| `statusLineOutput` | Token output/cache output segment | -| `statusLineCost` | Cost segment | -| `statusLineSubagents` | Subagent count segment | +| Token | Purpose | +| --------------------- | --------------------------------------- | +| `statusLineBg` | Status line background | +| `statusLineSep` | Separators between status line segments | +| `statusLineModel` | Model segment text | +| `statusLinePath` | Working directory segment | +| `statusLineGitClean` | Git segment (clean) | +| `statusLineGitDirty` | Git segment (dirty) | +| `statusLineContext` | Context window usage segment | +| `statusLineSpend` | Token input/total segment | +| `statusLineStaged` | Git staged count | +| `statusLineDirty` | Git unstaged count | +| `statusLineUntracked` | Git untracked count | +| `statusLineOutput` | Token output/cache output segment | +| `statusLineCost` | Cost segment | +| `statusLineSubagents` | Subagent count segment | **Total: 66 color tokens** (all required) @@ -280,20 +280,20 @@ On first run, OMP uses the terminal background reported by `COLORFGBG` and falls ### Theme Locations -Custom themes are loaded from `~/.omp/agent/themes/*.json` by default, or from `$OMP_CODING_AGENT_DIR/themes` if that environment variable is set. +Custom themes are loaded from `~/.omp/agent/themes/*.json` by default, or from `$PI_CODING_AGENT_DIR/themes` if that environment variable is set. ### Creating a Custom Theme 1. **Create theme directory:** ```bash - mkdir -p "${OMP_CODING_AGENT_DIR:-~/.omp/agent}/themes" + mkdir -p "${PI_CODING_AGENT_DIR:-~/.omp/agent}/themes" ``` 2. **Create theme file:** ```bash - vim "${OMP_CODING_AGENT_DIR:-~/.omp/agent}/themes/my-theme.json" + vim "${PI_CODING_AGENT_DIR:-~/.omp/agent}/themes/my-theme.json" ``` 3. **Define all colors (see the schema for the full list; snippet below shows structure):** @@ -573,12 +573,9 @@ export async function initTheme( themeName?: string, enableWatcher?: boolean, symbolPreset?: SymbolPreset, - colorBlindMode?: boolean, + colorBlindMode?: boolean ): Promise; -export async function setTheme( - name: string, - enableWatcher?: boolean, -): Promise<{ success: boolean; error?: string }>; +export async function setTheme(name: string, enableWatcher?: boolean): Promise<{ success: boolean; error?: string }>; // Usage throughout coding-agent import { theme } from "./theme.js"; @@ -629,7 +626,9 @@ export function getMarkdownTheme(): MarkdownTheme { strikethrough: (text) => chalk.strikethrough(text), symbols: getSymbolTheme(), getMermaidImage, - highlightCode: (code, lang) => { /* uses native syntax highlighter */ }, + highlightCode: (code, lang) => { + /* uses native syntax highlighter */ + }, }; } ``` @@ -688,7 +687,6 @@ const userMsg = theme.bg("userMessageBg", theme.fg("userMessageText", "Hello")); ``` 4. **Convert colors to ANSI codes based on terminal capability:** - - Empty string (`""`) → terminal default (foreground/background reset) - 256-color (`42`) → `\x1b[38;5;42m` / `\x1b[48;5;42m` - Hex or resolved vars → `Bun.color(value, "ansi-16m" | "ansi-256")` diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 3f3dc0dbd..2aeb6dcc4 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -74,7 +74,7 @@ "scripts": { "check": "tsgo -p tsconfig.json", "format-prompts": "bun scripts/format-prompts.ts", - "build:binary": "cd ../.. && bun --cwd=packages/natives run embed:native && bun build --compile --define OMP_COMPILED=true --root . ./packages/coding-agent/src/cli.ts --outfile packages/coding-agent/dist/omp && bun --cwd=packages/natives run embed:native --reset", + "build:binary": "cd ../.. && bun --cwd=packages/natives run embed:native && bun build --compile --define PI_COMPILED=true --root . ./packages/coding-agent/src/cli.ts --outfile packages/coding-agent/dist/omp && bun --cwd=packages/natives run embed:native --reset", "generate-template": "bun scripts/generate-template.ts", "test": "bun test" }, diff --git a/packages/coding-agent/src/capability/index.ts b/packages/coding-agent/src/capability/index.ts index e2609abc8..f9607dd7c 100644 --- a/packages/coding-agent/src/capability/index.ts +++ b/packages/coding-agent/src/capability/index.ts @@ -8,9 +8,10 @@ */ import * as os from "node:os"; import * as path from "node:path"; +import { getEnv } from "@oh-my-pi/pi-utils"; -/** Conditional startup debug prints (stderr) when OMP_DEBUG_STARTUP is set */ -const debugStartup = process.env.OMP_DEBUG_STARTUP +/** Conditional startup debug prints (stderr) when PI_DEBUG_STARTUP is set */ +const debugStartup = getEnv("PI_DEBUG_STARTUP") ? (stage: string) => process.stderr.write(`[startup] ${stage}\n`) : () => {}; diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index 1c9cc29a9..550648cd2 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -195,9 +195,9 @@ ${chalk.bold("Subcommands:")} ${chalk.bold("Options:")} --model Model to use (fuzzy match: "opus", "gpt-5.2", or "p-openai/gpt-5.2") - --smol Smol/fast model for lightweight tasks (or OMP_SMOL_MODEL env) - --slow Slow/reasoning model for thorough analysis (or OMP_SLOW_MODEL env) - --plan Plan model for architectural planning (or OMP_PLAN_MODEL env) + --smol Smol/fast model for lightweight tasks (or PI_SMOL_MODEL env) + --slow Slow/reasoning model for thorough analysis (or PI_SLOW_MODEL env) + --plan Plan model for architectural planning (or PI_PLAN_MODEL env) --api-key API key (defaults to env vars) --system-prompt System prompt (default: coding assistant prompt) --append-system-prompt Append text or file contents to the system prompt @@ -286,7 +286,7 @@ ${chalk.bold("Environment Variables:")} PERPLEXITY_API_KEY - Perplexity search API key ${chalk.dim("# Configuration")} - OMP_CODING_AGENT_DIR - Session storage directory (default: ~/${CONFIG_DIR_NAME}/agent) + PI_CODING_AGENT_DIR - Session storage directory (default: ~/${CONFIG_DIR_NAME}/agent) ${chalk.bold("Available Tools (all enabled by default):")} read - Read file contents diff --git a/packages/coding-agent/src/cli/grep-cli.ts b/packages/coding-agent/src/cli/grep-cli.ts index 507200132..af9d83a8d 100644 --- a/packages/coding-agent/src/cli/grep-cli.ts +++ b/packages/coding-agent/src/cli/grep-cli.ts @@ -142,7 +142,7 @@ ${chalk.bold("Options:")} -h, --help Show this help ${chalk.bold("Environment:")} - OMP_GREP_WORKERS=0 Disable worker pool (use single-threaded mode) + PI_GREP_WORKERS=0 Disable worker pool (use single-threaded mode) ${chalk.bold("Examples:")} ${APP_NAME} grep "import" src/ diff --git a/packages/coding-agent/src/commit/agentic/index.ts b/packages/coding-agent/src/commit/agentic/index.ts index 971b9c71b..5292e1a56 100644 --- a/packages/coding-agent/src/commit/agentic/index.ts +++ b/packages/coding-agent/src/commit/agentic/index.ts @@ -1,5 +1,6 @@ import * as path from "node:path"; import { createInterface } from "node:readline/promises"; +import { getEnv } from "@oh-my-pi/pi-utils"; import { applyChangelogProposals } from "../../commit/changelog"; import { detectChangelogBoundaries } from "../../commit/changelog/detect"; import { parseUnreleasedSection } from "../../commit/changelog/parse"; @@ -82,7 +83,7 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise { } else { writeStdout(" └─ (none found)"); } - const forceFallback = process.env.OMP_COMMIT_TEST_FALLBACK?.toLowerCase() === "true"; + const forceFallback = getEnv("PI_COMMIT_TEST_FALLBACK")?.toLowerCase() === "true"; if (forceFallback) { writeStdout("● Forcing fallback commit generation..."); const fallbackProposal = generateFallbackProposal(numstat); @@ -137,7 +138,7 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise { } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); writeStderr(`Agent error: ${errorMessage}`); - if (error instanceof Error && error.stack && process.env.DEBUG) { + if (error instanceof Error && error.stack && getEnv("DEBUG")) { writeStderr(error.stack); } writeStdout("● Using fallback commit generation..."); @@ -146,7 +147,7 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise { } if (!usedFallback && !commitState.proposal && !commitState.splitProposal) { - if (process.env.OMP_COMMIT_NO_FALLBACK?.toLowerCase() !== "true") { + if (getEnv("PI_COMMIT_NO_FALLBACK")?.toLowerCase() !== "true") { writeStdout("● Agent did not provide proposal, using fallback..."); commitState.proposal = generateFallbackProposal(numstat); usedFallback = true; diff --git a/packages/coding-agent/src/commit/map-reduce/index.ts b/packages/coding-agent/src/commit/map-reduce/index.ts index 41f6e4d14..2e4f62cb1 100644 --- a/packages/coding-agent/src/commit/map-reduce/index.ts +++ b/packages/coding-agent/src/commit/map-reduce/index.ts @@ -1,4 +1,5 @@ import type { Api, Model } from "@oh-my-pi/pi-ai"; +import { getEnv } from "@oh-my-pi/pi-utils"; import { parseFileDiffs } from "../../commit/git/diff"; import type { ConventionalAnalysis } from "../../commit/types"; import { isExcludedFile } from "../../commit/utils/exclusions"; @@ -30,7 +31,7 @@ export interface MapReduceInput { } export function shouldUseMapReduce(diff: string, settings?: MapReduceSettings): boolean { - if (process.env.OMP_COMMIT_MAP_REDUCE?.toLowerCase() === "false") return false; + if (getEnv("PI_COMMIT_MAP_REDUCE")?.toLowerCase() === "false") return false; if (settings?.enabled === false) return false; const minFiles = settings?.minFiles ?? MIN_FILES_FOR_MAP_REDUCE; const maxFileTokens = settings?.maxFileTokens ?? MAX_FILE_TOKENS; diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index eeb77fadc..fe39badbb 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -1,7 +1,7 @@ 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 { getEnv, isEnoent, logger } from "@oh-my-pi/pi-utils"; // Embed package.json at build time for config import packageJson from "../package.json" with { type: "json" }; @@ -53,7 +53,7 @@ export function getChangelogPath(): string { /** Get the agent config directory (e.g., ~/.omp/agent/) */ export function getAgentDir(): string { - return process.env.OMP_CODING_AGENT_DIR || path.join(os.homedir(), CONFIG_DIR_NAME, "agent"); + return getEnv("PI_CODING_AGENT_DIR") || path.join(os.homedir(), CONFIG_DIR_NAME, "agent"); } /** Get path to user's custom themes directory */ diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index 6f60069f6..5c26a1c8c 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -246,7 +246,6 @@ export const SETTINGS_SCHEMA = { enabledModels: { type: "array", default: [] as string[] }, disabledProviders: { type: "array", default: [] as string[] }, disabledExtensions: { type: "array", default: [] as string[] }, - env: { type: "record", default: {} as Record }, modelRoles: { type: "record", default: {} as Record }, // ───────────────────────────────────────────────────────────────────────── diff --git a/packages/coding-agent/src/config/settings.ts b/packages/coding-agent/src/config/settings.ts index 7ac885daf..7cf414f25 100644 --- a/packages/coding-agent/src/config/settings.ts +++ b/packages/coding-agent/src/config/settings.ts @@ -449,10 +449,6 @@ export class Settings { // Build merged view this.rebuildMerged(); - - // Apply environment variables - this.applyEnvironmentVariables(); - return this; } @@ -550,16 +546,6 @@ export class Settings { return raw; } - private applyEnvironmentVariables(): void { - const env = this.get("env") as Record; - if (!env) return; - for (const [key, value] of Object.entries(env)) { - if (typeof key === "string" && typeof value === "string" && !(key in process.env)) { - process.env[key] = value; - } - } - } - // ───────────────────────────────────────────────────────────────────────── // Saving // ───────────────────────────────────────────────────────────────────────── diff --git a/packages/coding-agent/src/exa/mcp-client.ts b/packages/coding-agent/src/exa/mcp-client.ts index dcb2de92e..a0430c32d 100644 --- a/packages/coding-agent/src/exa/mcp-client.ts +++ b/packages/coding-agent/src/exa/mcp-client.ts @@ -4,7 +4,7 @@ * Client for interacting with Exa MCP servers. */ import * as os from "node:os"; -import { isEnoent, logger } from "@oh-my-pi/pi-utils"; +import { getEnv, isEnoent, logger } from "@oh-my-pi/pi-utils"; import type { TSchema } from "@sinclair/typebox"; import type { CustomTool, CustomToolResult } from "../extensibility/custom-tools/types"; import { callMCP } from "../mcp/json-rpc"; @@ -21,8 +21,8 @@ import type { /** Find EXA_API_KEY from process.env or .env files */ export async function findApiKey(): Promise { // Check process.env first - if (process.env.EXA_API_KEY) { - return process.env.EXA_API_KEY; + if (getEnv("EXA_API_KEY")) { + return getEnv("EXA_API_KEY"); } // Try loading from .env files in cwd and home diff --git a/packages/coding-agent/src/ipy/executor.ts b/packages/coding-agent/src/ipy/executor.ts index a4b928273..2b809caeb 100644 --- a/packages/coding-agent/src/ipy/executor.ts +++ b/packages/coding-agent/src/ipy/executor.ts @@ -1,5 +1,5 @@ import * as path from "node:path"; -import { isEnoent, logger } from "@oh-my-pi/pi-utils"; +import { getEnv, isEnoent, logger } from "@oh-my-pi/pi-utils"; import { getAgentDir } from "../config"; import { OutputSink } from "../session/streaming-output"; import { time } from "../utils/timings"; @@ -15,7 +15,7 @@ import { import { discoverPythonModules } from "./modules"; import { PYTHON_PRELUDE } from "./prelude"; -const debugStartup = process.env.OMP_DEBUG_STARTUP +const debugStartup = getEnv("PI_DEBUG_STARTUP") ? (stage: string) => process.stderr.write(`[startup] ${stage}\n`) : () => {}; @@ -327,7 +327,7 @@ async function createKernelSession( const env: Record | undefined = sessionFile || artifactsDir ? { - ...(sessionFile ? { OMP_SESSION_FILE: sessionFile } : {}), + ...(sessionFile ? { PI_SESSION_FILE: sessionFile } : {}), ...(artifactsDir ? { ARTIFACTS: artifactsDir } : {}), } : undefined; @@ -384,7 +384,7 @@ async function restartKernelSession( const env: Record | undefined = sessionFile || artifactsDir ? { - ...(sessionFile ? { OMP_SESSION_FILE: sessionFile } : {}), + ...(sessionFile ? { PI_SESSION_FILE: sessionFile } : {}), ...(artifactsDir ? { ARTIFACTS: artifactsDir } : {}), } : undefined; @@ -537,7 +537,7 @@ export async function executePython(code: string, options?: PythonExecutorOption const env: Record | undefined = sessionFile || artifactsDir ? { - ...(sessionFile ? { OMP_SESSION_FILE: sessionFile } : {}), + ...(sessionFile ? { PI_SESSION_FILE: sessionFile } : {}), ...(artifactsDir ? { ARTIFACTS: artifactsDir } : {}), } : undefined; diff --git a/packages/coding-agent/src/ipy/gateway-coordinator.ts b/packages/coding-agent/src/ipy/gateway-coordinator.ts index 54740825f..4b07807f8 100644 --- a/packages/coding-agent/src/ipy/gateway-coordinator.ts +++ b/packages/coding-agent/src/ipy/gateway-coordinator.ts @@ -243,7 +243,7 @@ async function startGatewayProcess( const kernelEnv: Record = { ...runtime.env, PYTHONUNBUFFERED: "1", - OMP_SHELL_SNAPSHOT: snapshotPath ?? undefined, + PI_SHELL_SNAPSHOT: snapshotPath ?? undefined, }; const gatewayPort = await allocatePort(); diff --git a/packages/coding-agent/src/ipy/kernel.ts b/packages/coding-agent/src/ipy/kernel.ts index 05476e897..43aab6148 100644 --- a/packages/coding-agent/src/ipy/kernel.ts +++ b/packages/coding-agent/src/ipy/kernel.ts @@ -1,4 +1,4 @@ -import { logger } from "@oh-my-pi/pi-utils"; +import { getEnv, logger } from "@oh-my-pi/pi-utils"; import { $ } from "bun"; import { nanoid } from "nanoid"; import { Settings } from "../config/settings"; @@ -11,10 +11,10 @@ import { filterEnv, resolvePythonRuntime } from "./runtime"; const TEXT_ENCODER = new TextEncoder(); const TEXT_DECODER = new TextDecoder(); -const TRACE_IPC = process.env.OMP_PYTHON_IPC_TRACE === "1"; +const TRACE_IPC = getEnv("PI_PYTHON_IPC_TRACE") === "1"; const PRELUDE_INTROSPECTION_SNIPPET = "import json\nprint(json.dumps(__omp_prelude_docs__()))"; -const debugStartup = process.env.OMP_DEBUG_STARTUP +const debugStartup = getEnv("PI_DEBUG_STARTUP") ? (stage: string) => process.stderr.write(`[startup] ${stage}\n`) : () => {}; @@ -33,11 +33,11 @@ interface ExternalGatewayConfig { } function getExternalGatewayConfig(): ExternalGatewayConfig | null { - const url = process.env.OMP_PYTHON_GATEWAY_URL; + const url = getEnv("PI_PYTHON_GATEWAY_URL"); if (!url) return null; return { url: url.replace(/\/$/, ""), - token: process.env.OMP_PYTHON_GATEWAY_TOKEN, + token: getEnv("PI_PYTHON_GATEWAY_TOKEN"), }; } @@ -111,7 +111,7 @@ export interface PythonKernelAvailability { } export async function checkPythonKernelAvailability(cwd: string): Promise { - if (process.env.BUN_ENV === "test" || process.env.NODE_ENV === "test" || process.env.OMP_PYTHON_SKIP_CHECK === "1") { + if (process.env.BUN_ENV === "test" || process.env.NODE_ENV === "test" || getEnv("PI_PYTHON_SKIP_CHECK") === "1") { return { ok: true }; } @@ -163,7 +163,7 @@ async function checkExternalGatewayAvailability(config: ExternalGatewayConfig): if (response.status === 401 || response.status === 403) { return { ok: false, - reason: `External gateway at ${config.url} requires authentication. Set OMP_PYTHON_GATEWAY_TOKEN.`, + reason: `External gateway at ${config.url} requires authentication. Set PI_PYTHON_GATEWAY_TOKEN.`, }; } diff --git a/packages/coding-agent/src/ipy/prelude.py b/packages/coding-agent/src/ipy/prelude.py index 1e8cfd196..af30b4ba3 100644 --- a/packages/coding-agent/src/ipy/prelude.py +++ b/packages/coding-agent/src/ipy/prelude.py @@ -640,7 +640,7 @@ if "__omp_prelude_loaded__" not in globals(): output('explore_0', offset=10, limit=20) # Lines 10-29 output('explore_0', 'reviewer_1') # Read multiple outputs """ - session_file = os.environ.get("OMP_SESSION_FILE") + session_file = os.environ.get("PI_SESSION_FILE") if not session_file: _emit_status("output", error="No session file available") raise RuntimeError("No session - output artifacts unavailable") diff --git a/packages/coding-agent/src/ipy/runtime.ts b/packages/coding-agent/src/ipy/runtime.ts index a7c580a75..de3aaaa02 100644 --- a/packages/coding-agent/src/ipy/runtime.ts +++ b/packages/coding-agent/src/ipy/runtime.ts @@ -7,6 +7,8 @@ import * as fs from "node:fs"; import * as path from "node:path"; +import { getEnv } from "@oh-my-pi/pi-utils"; + const DEFAULT_ENV_ALLOWLIST = new Set([ "PATH", "HOME", @@ -76,7 +78,7 @@ const DEFAULT_ENV_DENYLIST = new Set([ "MISTRAL_API_KEY", ]); -const DEFAULT_ENV_ALLOW_PREFIXES = ["LC_", "XDG_", "OMP_"]; +const DEFAULT_ENV_ALLOW_PREFIXES = ["LC_", "XDG_", "PI_"]; const CASE_INSENSITIVE_ENV = process.platform === "win32"; const BASE_ENV_ALLOWLIST = new Set([...DEFAULT_ENV_ALLOWLIST, ...WINDOWS_ENV_ALLOWLIST]); @@ -107,7 +109,7 @@ export interface PythonRuntime { /** Filtered environment variables */ env: Record; /** Path to virtual environment, if detected */ - venvPath: string | null; + venvPath?: string; } /** @@ -135,15 +137,15 @@ export function filterEnv(env: Record): Record { * Detect lspmux availability and state. * Results are cached for STATE_CACHE_TTL_MS. * - * Set OMP_DISABLE_LSPMUX=1 to disable. + * Set PI_DISABLE_LSPMUX=1 to disable. */ export async function detectLspmux(): Promise { const now = Date.now(); @@ -135,7 +135,7 @@ export async function detectLspmux(): Promise { return cachedState; } - if (process.env.OMP_DISABLE_LSPMUX === "1") { + if (getEnv("PI_DISABLE_LSPMUX") === "1") { cachedState = { available: false, running: false, binaryPath: null, config: null }; cacheTimestamp = now; return cachedState; diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 4c4435af6..e9ba5d54b 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -9,7 +9,7 @@ import * as os from "node:os"; import * as path from "node:path"; import { createInterface } from "node:readline/promises"; import { type ImageContent, supportsXhigh } from "@oh-my-pi/pi-ai"; -import { postmortem } from "@oh-my-pi/pi-utils"; +import { getEnv, postmortem } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { type Args, parseArgs, printHelp } from "./cli/args"; import { parseConfigArgs, printConfigHelp, runConfigCommand } from "./cli/config-cli"; @@ -42,8 +42,8 @@ import { resolvePromptInput } from "./system-prompt"; import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog"; import { printTimings, time } from "./utils/timings"; -/** Conditional startup debug prints (stderr) when OMP_DEBUG_STARTUP is set */ -const debugStartup = process.env.OMP_DEBUG_STARTUP +/** Conditional startup debug prints (stderr) when PI_DEBUG_STARTUP is set */ +const debugStartup = getEnv("PI_DEBUG_STARTUP") ? (stage: string) => process.stderr.write(`[startup] ${stage}\n`) : () => {}; @@ -655,9 +655,9 @@ export async function main(args: string[]) { time("initializeWithSettings"); // Apply model role overrides from CLI args or env vars (ephemeral, not persisted) - const smolModel = parsed.smol ?? process.env.OMP_SMOL_MODEL; - const slowModel = parsed.slow ?? process.env.OMP_SLOW_MODEL; - const planModel = parsed.plan ?? process.env.OMP_PLAN_MODEL; + const smolModel = parsed.smol ?? getEnv("PI_SMOL_MODEL"); + const slowModel = parsed.slow ?? getEnv("PI_SLOW_MODEL"); + const planModel = parsed.plan ?? getEnv("PI_PLAN_MODEL"); if (smolModel || slowModel || planModel) { const currentRoles = settings.get("modelRoles") as Record; if (smolModel) currentRoles.smol = smolModel; diff --git a/packages/coding-agent/src/modes/components/hook-editor.ts b/packages/coding-agent/src/modes/components/hook-editor.ts index ed2f7d8b7..54fd05f94 100644 --- a/packages/coding-agent/src/modes/components/hook-editor.ts +++ b/packages/coding-agent/src/modes/components/hook-editor.ts @@ -7,6 +7,7 @@ import * as os from "node:os"; import * as path from "node:path"; import { Container, Editor, matchesKey, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui"; import { nanoid } from "nanoid"; +import { getEnv } from "@oh-my-pi/pi-utils"; import { getEditorTheme, theme } from "../../modes/theme/theme"; import { DynamicBorder } from "./dynamic-border"; @@ -47,7 +48,7 @@ export class HookEditorComponent extends Container { this.addChild(new Spacer(1)); // Add hint - const hasExternalEditor = !!(process.env.VISUAL || process.env.EDITOR); + const hasExternalEditor = !!(getEnv("VISUAL") || getEnv("EDITOR")); const hint = hasExternalEditor ? "ctrl+enter submit esc cancel ctrl+g external editor" : "ctrl+enter submit esc cancel"; @@ -83,7 +84,7 @@ export class HookEditorComponent extends Container { } private async openExternalEditor(): Promise { - const editorCmd = process.env.VISUAL || process.env.EDITOR; + const editorCmd = getEnv("VISUAL") || getEnv("EDITOR"); if (!editorCmd) { return; } diff --git a/packages/coding-agent/src/modes/controllers/input-controller.ts b/packages/coding-agent/src/modes/controllers/input-controller.ts index 11af5745a..611b1838c 100644 --- a/packages/coding-agent/src/modes/controllers/input-controller.ts +++ b/packages/coding-agent/src/modes/controllers/input-controller.ts @@ -4,6 +4,7 @@ import * as os from "node:os"; import * as path from "node:path"; import type { AgentMessage } from "@oh-my-pi/pi-agent-core"; import { readImageFromClipboard } from "@oh-my-pi/pi-natives"; +import { getEnv } from "@oh-my-pi/pi-utils"; import { nanoid } from "nanoid"; import type { SettingPath, SettingValue } from "../../config/settings"; import { settings } from "../../config/settings"; @@ -460,7 +461,7 @@ export class InputController { // Generate session title on first message const hasUserMessages = this.ctx.agent.state.messages.some((m: AgentMessage) => m.role === "user"); - if (!hasUserMessages && !this.ctx.sessionManager.getSessionName() && !process.env.OMP_NO_TITLE) { + if (!hasUserMessages && !this.ctx.sessionManager.getSessionName() && !getEnv("PI_NO_TITLE")) { const registry = this.ctx.session.modelRegistry; const smolModel = this.ctx.settings.getModelRole("smol"); generateSessionTitle(text, registry, smolModel, this.ctx.session.sessionId) @@ -731,7 +732,7 @@ export class InputController { async openExternalEditor(): Promise { // Determine editor (respect $VISUAL, then $EDITOR) - const editorCmd = process.env.VISUAL || process.env.EDITOR; + const editorCmd = getEnv("VISUAL") || getEnv("EDITOR"); if (!editorCmd) { this.ctx.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable."); return; diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index 61412af5f..e54d08833 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -15,7 +15,7 @@ import { Text, TUI, } from "@oh-my-pi/pi-tui"; -import { isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils"; +import { getEnv, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { KeybindingsManager } from "../config/keybindings"; import { renderPromptTemplate } from "../config/prompt-templates"; @@ -53,8 +53,8 @@ import { getEditorTheme, getMarkdownTheme, onThemeChange, theme } from "./theme/ import type { CompactionQueuedMessage, InteractiveModeContext, TodoItem } from "./types"; import { UiHelpers } from "./utils/ui-helpers"; -/** Conditional startup debug prints (stderr) when OMP_DEBUG_STARTUP is set */ -const debugStartup = process.env.OMP_DEBUG_STARTUP +/** Conditional startup debug prints (stderr) when PI_DEBUG_STARTUP is set */ +const debugStartup = getEnv("PI_DEBUG_STARTUP") ? (stage: string) => process.stderr.write(`[startup] ${stage}\n`) : () => {}; diff --git a/packages/coding-agent/src/patch/index.ts b/packages/coding-agent/src/patch/index.ts index c9b6508ac..b670cd58c 100644 --- a/packages/coding-agent/src/patch/index.ts +++ b/packages/coding-agent/src/patch/index.ts @@ -215,14 +215,14 @@ export class EditTool implements AgentTool { this.session = session; const { - OMP_EDIT_FUZZY: editFuzzy = "auto", - OMP_EDIT_FUZZY_THRESHOLD: editFuzzyThreshold = "auto", - OMP_EDIT_VARIANT: envEditVariant = "auto", + PI_EDIT_FUZZY: editFuzzy = "auto", + PI_EDIT_FUZZY_THRESHOLD: editFuzzyThreshold = "auto", + PI_EDIT_VARIANT: envEditVariant = "auto", } = process.env; this.envEditVariant = envEditVariant; if (envEditVariant !== "replace" && envEditVariant !== "patch" && envEditVariant !== "auto") { - throw new Error(`Invalid OMP_EDIT_VARIANT: ${envEditVariant}`); + throw new Error(`Invalid PI_EDIT_VARIANT: ${envEditVariant}`); } switch (editFuzzy) { @@ -238,7 +238,7 @@ export class EditTool implements AgentTool { this.allowFuzzy = session.settings.get("edit.fuzzyMatch"); break; default: - throw new Error(`Invalid OMP_EDIT_FUZZY: ${editFuzzy}`); + throw new Error(`Invalid PI_EDIT_FUZZY: ${editFuzzy}`); } switch (editFuzzyThreshold) { case "auto": @@ -247,7 +247,7 @@ export class EditTool implements AgentTool { default: this.fuzzyThreshold = parseFloat(editFuzzyThreshold); if (Number.isNaN(this.fuzzyThreshold) || this.fuzzyThreshold < 0 || this.fuzzyThreshold > 1) { - throw new Error(`Invalid OMP_EDIT_FUZZY_THRESHOLD: ${editFuzzyThreshold}`); + throw new Error(`Invalid PI_EDIT_FUZZY_THRESHOLD: ${editFuzzyThreshold}`); } break; } diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index cee786723..d4622ce62 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -30,7 +30,7 @@ import * as path from "node:path"; import { Agent, type AgentEvent, type AgentMessage, type AgentTool, type ThinkingLevel } from "@oh-my-pi/pi-agent-core"; import { type Message, type Model, supportsXhigh } from "@oh-my-pi/pi-ai"; import type { Component } from "@oh-my-pi/pi-tui"; -import { logger, postmortem } from "@oh-my-pi/pi-utils"; +import { getEnv, logger, postmortem } from "@oh-my-pi/pi-utils"; import { YAML } from "bun"; import chalk from "chalk"; import { loadCapability } from "./capability"; @@ -111,8 +111,8 @@ import { wrapToolsWithMetaNotice } from "./tools/output-meta"; import { EventBus } from "./utils/event-bus"; import { time } from "./utils/timings"; -/** Conditional startup debug prints (stderr) when OMP_DEBUG_STARTUP is set */ -const debugStartup = process.env.OMP_DEBUG_STARTUP +/** Conditional startup debug prints (stderr) when PI_DEBUG_STARTUP is set */ +const debugStartup = getEnv("PI_DEBUG_STARTUP") ? (stage: string) => process.stderr.write(`[startup] ${stage}\n`) : () => {}; @@ -815,7 +815,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} toolSession.mcpManager = mcpManager; // If we extracted Exa API keys from MCP configs and EXA_API_KEY isn't set, use the first one - if (mcpResult.exaApiKeys.length > 0 && !process.env.EXA_API_KEY) { + if (mcpResult.exaApiKeys.length > 0 && !getEnv("EXA_API_KEY")) { process.env.EXA_API_KEY = mcpResult.exaApiKeys[0]; } diff --git a/packages/coding-agent/src/ssh/connection-manager.ts b/packages/coding-agent/src/ssh/connection-manager.ts index ef254f0ae..cb99c1a5e 100644 --- a/packages/coding-agent/src/ssh/connection-manager.ts +++ b/packages/coding-agent/src/ssh/connection-manager.ts @@ -306,12 +306,12 @@ async function probeHostInfo(host: SSHConnectionTarget): Promise { const hasBash = !unexpandedPosixVars && (Boolean(bashVersion) || shell === "bash"); let compatShell: SSHHostInfo["compatShell"]; if (os === "windows" && host.compat !== false) { - const bashProbe = await runSshCaptureSync(await buildRemoteCommand(host, 'bash -lc "echo OMP_BASH_OK"')); - if (bashProbe.exitCode === 0 && bashProbe.stdout.includes("OMP_BASH_OK")) { + const bashProbe = await runSshCaptureSync(await buildRemoteCommand(host, 'bash -lc "echo PI_BASH_OK"')); + if (bashProbe.exitCode === 0 && bashProbe.stdout.includes("PI_BASH_OK")) { compatShell = "bash"; } else { - const shProbe = await runSshCaptureSync(await buildRemoteCommand(host, 'sh -lc "echo OMP_SH_OK"')); - if (shProbe.exitCode === 0 && shProbe.stdout.includes("OMP_SH_OK")) { + const shProbe = await runSshCaptureSync(await buildRemoteCommand(host, 'sh -lc "echo PI_SH_OK"')); + if (shProbe.exitCode === 0 && shProbe.stdout.includes("PI_SH_OK")) { compatShell = "sh"; } } diff --git a/packages/coding-agent/src/system-prompt.ts b/packages/coding-agent/src/system-prompt.ts index a1f51b664..6f725a46f 100644 --- a/packages/coding-agent/src/system-prompt.ts +++ b/packages/coding-agent/src/system-prompt.ts @@ -3,6 +3,7 @@ */ import * as os from "node:os"; import * as path from "node:path"; +import { getEnv } from "@oh-my-pi/pi-utils"; import { getSystemInfo as getNativeSystemInfo, type SystemInfo } from "@oh-my-pi/pi-natives"; import { $ } from "bun"; import chalk from "chalk"; @@ -448,7 +449,7 @@ export interface BuildSystemPromptOptions { /** Build the system prompt with tools, guidelines, and context */ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise { - if (process.env.NULL_PROMPT === "true") { + if (getEnv("NULL_PROMPT") === "true") { return ""; } diff --git a/packages/coding-agent/src/task/index.ts b/packages/coding-agent/src/task/index.ts index df7dfc8ae..e3fe86835 100644 --- a/packages/coding-agent/src/task/index.ts +++ b/packages/coding-agent/src/task/index.ts @@ -17,6 +17,7 @@ import * as os from "node:os"; import path from "node:path"; import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core"; import type { Usage } from "@oh-my-pi/pi-ai"; +import { getEnv } from "@oh-my-pi/pi-utils"; import { $ } from "bun"; import { nanoid } from "nanoid"; import type { ToolSession } from ".."; @@ -135,7 +136,7 @@ export class TaskTool implements AgentTool }; /** Maximum output bytes per agent */ -export const MAX_OUTPUT_BYTES = parseNumber(getEnv("OMP_TASK_MAX_OUTPUT_BYTES"), 500_000); +export const MAX_OUTPUT_BYTES = parseNumber(getEnv("PI_TASK_MAX_OUTPUT_BYTES"), 500_000); /** Maximum output lines per agent */ -export const MAX_OUTPUT_LINES = parseNumber(getEnv("OMP_TASK_MAX_OUTPUT_LINES"), 5000); +export const MAX_OUTPUT_LINES = parseNumber(getEnv("PI_TASK_MAX_OUTPUT_LINES"), 5000); /** EventBus channel for raw subagent events */ export const TASK_SUBAGENT_EVENT_CHANNEL = "task:subagent:event"; diff --git a/packages/coding-agent/src/tools/index.ts b/packages/coding-agent/src/tools/index.ts index a9aa27692..edde6168a 100644 --- a/packages/coding-agent/src/tools/index.ts +++ b/packages/coding-agent/src/tools/index.ts @@ -1,5 +1,5 @@ import type { AgentTool } from "@oh-my-pi/pi-agent-core"; -import { logger } from "@oh-my-pi/pi-utils"; +import { getEnv, logger } from "@oh-my-pi/pi-utils"; import type { PromptTemplate } from "../config/prompt-templates"; import type { Settings } from "../config/settings"; import type { Skill } from "../extensibility/skills"; @@ -194,7 +194,7 @@ export type ToolName = keyof typeof BUILTIN_TOOLS; export type PythonToolMode = "ipy-only" | "bash-only" | "both"; /** - * Parse OMP_PY environment variable to determine Python tool mode. + * Parse PI_PY environment variable to determine Python tool mode. * Returns null if not set or invalid. * * Values: @@ -203,7 +203,7 @@ export type PythonToolMode = "ipy-only" | "bash-only" | "both"; * - "mix" or "both" → both */ function getPythonModeFromEnv(): PythonToolMode | null { - const value = process.env.OMP_PY?.toLowerCase(); + const value = getEnv("PI_PY")?.toLowerCase(); if (!value) return null; switch (value) { @@ -240,7 +240,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P pythonMode !== "bash-only" && (requestedTools === undefined || requestedTools.includes("python")); const isTestEnv = process.env.BUN_ENV === "test" || process.env.NODE_ENV === "test"; - const skipPythonWarm = isTestEnv || process.env.OMP_PYTHON_SKIP_CHECK === "1"; + const skipPythonWarm = isTestEnv || getEnv("PI_PYTHON_SKIP_CHECK") === "1"; if (shouldCheckPython) { const availability = await checkPythonKernelAvailability(session.cwd); time("createTools:pythonCheck"); @@ -324,7 +324,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P }), ); time("createTools:afterFactories"); - if (slowTools.length > 0 && process.env.OMP_TIMING === "1") { + if (slowTools.length > 0 && getEnv("PI_TIMING") === "1") { logger.debug("Tool factory timings", { slowTools }); } const tools = results.filter(r => r.tool !== null).map(r => r.tool as Tool); diff --git a/packages/coding-agent/src/utils/timings.ts b/packages/coding-agent/src/utils/timings.ts index c1df9eda1..ee8f21a38 100644 --- a/packages/coding-agent/src/utils/timings.ts +++ b/packages/coding-agent/src/utils/timings.ts @@ -1,9 +1,10 @@ /** * Central timing instrumentation for startup profiling. - * Enable with OMP_TIMING=1 or PI_TIMING=1 environment variable. + * Enable with PI_TIMING=1 or PI_TIMING=1 environment variable. */ +import { getEnv } from "@oh-my-pi/pi-utils"; -const ENABLED = process.env.OMP_TIMING === "1"; +const ENABLED = getEnv("PI_TIMING") === "1"; const timings: Array<{ label: string; ms: number }> = []; let lastTime = Date.now(); diff --git a/packages/coding-agent/src/web/scrapers/github.ts b/packages/coding-agent/src/web/scrapers/github.ts index 7b32e00ee..13faf3e54 100644 --- a/packages/coding-agent/src/web/scrapers/github.ts +++ b/packages/coding-agent/src/web/scrapers/github.ts @@ -1,4 +1,4 @@ -import { ptree } from "@oh-my-pi/pi-utils"; +import { getEnv, ptree } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; import { finalizeOutput, loadPage } from "./types"; @@ -85,7 +85,7 @@ export async function fetchGitHubApi( }; // Use GITHUB_TOKEN if available - const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN; + const token = getEnv("GITHUB_TOKEN") || getEnv("GH_TOKEN"); if (token) { headers.Authorization = `Bearer ${token}`; } diff --git a/packages/coding-agent/test/bash-executor.test.ts b/packages/coding-agent/test/bash-executor.test.ts index 24fec8f5c..ad386aa88 100644 --- a/packages/coding-agent/test/bash-executor.test.ts +++ b/packages/coding-agent/test/bash-executor.test.ts @@ -40,10 +40,10 @@ describe("executeBash", () => { }); it("passes env vars", async () => { - const result = await executeBash("echo $OMP_TEST_ENV", { + const result = await executeBash("echo $PI_TEST_ENV", { cwd: tempDir, timeout: 5000, - env: { OMP_TEST_ENV: "hello" }, + env: { PI_TEST_ENV: "hello" }, }); expect(result.output.trim()).toBe("hello"); }); @@ -166,7 +166,7 @@ describe("executeBash", () => { return; } const snapshotPath = path.join(tempDir, "snapshot.sh"); - fs.writeFileSync(snapshotPath, "export OMP_SNAPSHOT_TEST=from_snapshot\n"); + fs.writeFileSync(snapshotPath, "export PI_SNAPSHOT_TEST=from_snapshot\n"); vi.spyOn(Settings.prototype, "getShellConfig").mockReturnValue({ shell: bashPath, args: ["-l", "-c"], @@ -179,7 +179,7 @@ describe("executeBash", () => { vi.spyOn(shellSnapshot, "getOrCreateSnapshot").mockResolvedValue(snapshotPath); const sessionKey = "snapshot-test"; await executeBash("true", { cwd: tempDir, timeout: 5000, sessionKey }); - const result = await executeBash("echo $OMP_SNAPSHOT_TEST", { cwd: tempDir, timeout: 5000, sessionKey }); + const result = await executeBash("echo $PI_SNAPSHOT_TEST", { cwd: tempDir, timeout: 5000, sessionKey }); expect(result.output.trim()).toBe("from_snapshot"); }); diff --git a/packages/coding-agent/test/core/python-executor-per-call.test.ts b/packages/coding-agent/test/core/python-executor-per-call.test.ts index 7134e377e..4dbeef5c6 100644 --- a/packages/coding-agent/test/core/python-executor-per-call.test.ts +++ b/packages/coding-agent/test/core/python-executor-per-call.test.ts @@ -11,7 +11,7 @@ interface KernelStub { describe("executePython (per-call)", () => { it("shuts down kernel on timed-out cancellation", async () => { - process.env.OMP_PYTHON_SKIP_CHECK = "1"; + process.env.PI_PYTHON_SKIP_CHECK = "1"; using tempDir = TempDir.createSync("@omp-python-executor-per-call-"); let shutdownCalls = 0; diff --git a/packages/coding-agent/test/core/python-executor.lifecycle.test.ts b/packages/coding-agent/test/core/python-executor.lifecycle.test.ts index adf47cb35..a1ad81e67 100644 --- a/packages/coding-agent/test/core/python-executor.lifecycle.test.ts +++ b/packages/coding-agent/test/core/python-executor.lifecycle.test.ts @@ -6,7 +6,7 @@ import { PythonKernel, } from "@oh-my-pi/pi-coding-agent/ipy/kernel"; -process.env.OMP_PYTHON_SKIP_CHECK = "1"; +process.env.PI_PYTHON_SKIP_CHECK = "1"; class FakeKernel { private result: KernelExecuteResult; diff --git a/packages/coding-agent/test/core/python-executor.test.ts b/packages/coding-agent/test/core/python-executor.test.ts index ef44f6b1e..a9a422495 100644 --- a/packages/coding-agent/test/core/python-executor.test.ts +++ b/packages/coding-agent/test/core/python-executor.test.ts @@ -160,8 +160,8 @@ afterEach(async () => { describe("warmPythonEnvironment", () => { it("caches prelude docs on warmup", async () => { - const previousSkip = process.env.OMP_PYTHON_SKIP_CHECK; - process.env.OMP_PYTHON_SKIP_CHECK = "1"; + const previousSkip = process.env.PI_PYTHON_SKIP_CHECK; + process.env.PI_PYTHON_SKIP_CHECK = "1"; using tempDir = TempDir.createSync("@python-executor-"); const docs: PreludeHelper[] = [ { @@ -188,9 +188,9 @@ describe("warmPythonEnvironment", () => { startSpy.mockRestore(); if (previousSkip === undefined) { - delete process.env.OMP_PYTHON_SKIP_CHECK; + delete process.env.PI_PYTHON_SKIP_CHECK; } else { - process.env.OMP_PYTHON_SKIP_CHECK = previousSkip; + process.env.PI_PYTHON_SKIP_CHECK = previousSkip; } }); }); diff --git a/packages/coding-agent/test/core/python-kernel-env.test.ts b/packages/coding-agent/test/core/python-kernel-env.test.ts index 02e111322..1097ed9e2 100644 --- a/packages/coding-agent/test/core/python-kernel-env.test.ts +++ b/packages/coding-agent/test/core/python-kernel-env.test.ts @@ -9,7 +9,7 @@ describe("Python gateway environment filtering", () => { OPENAI_API_KEY: "secret", ANTHROPIC_API_KEY: "also-secret", UNSAFE_TOKEN: "nope", - OMP_CUSTOM: "1", + PI_CUSTOM: "1", LC_ALL: "en_US.UTF-8", }; @@ -17,7 +17,7 @@ describe("Python gateway environment filtering", () => { expect(filtered.PATH).toBe("/bin"); expect(filtered.HOME).toBe("/home/test"); - expect(filtered.OMP_CUSTOM).toBe("1"); + expect(filtered.PI_CUSTOM).toBe("1"); expect(filtered.LC_ALL).toBe("en_US.UTF-8"); expect(filtered.OPENAI_API_KEY).toBeUndefined(); expect(filtered.ANTHROPIC_API_KEY).toBeUndefined(); @@ -45,12 +45,12 @@ describe("Python gateway environment filtering", () => { PATH: "/usr/bin", HOME: "/home/test", OPENAI_API_KEY: "secret", - OMP_DEBUG: "1", + PI_DEBUG: "1", }; const filtered = filterEnv(env); expect(filtered.OPENAI_API_KEY).toBeUndefined(); expect(filtered.PATH).toBe("/usr/bin"); - expect(filtered.OMP_DEBUG).toBe("1"); + expect(filtered.PI_DEBUG).toBe("1"); }); }); diff --git a/packages/coding-agent/test/core/python-kernel-session.test.ts b/packages/coding-agent/test/core/python-kernel-session.test.ts index 61dfc6c2f..e9b0944fd 100644 --- a/packages/coding-agent/test/core/python-kernel-session.test.ts +++ b/packages/coding-agent/test/core/python-kernel-session.test.ts @@ -40,7 +40,7 @@ describe("executePython kernel reuse", () => { let kernels: FakeKernel[] = []; beforeEach(() => { - process.env.OMP_PYTHON_SKIP_CHECK = "1"; + process.env.PI_PYTHON_SKIP_CHECK = "1"; startCalls = 0; kernels = []; PythonKernel.start = (async () => { diff --git a/packages/coding-agent/test/core/python-kernel.lifecycle.test.ts b/packages/coding-agent/test/core/python-kernel.lifecycle.test.ts index ef97acc0e..c5aa44d23 100644 --- a/packages/coding-agent/test/core/python-kernel.lifecycle.test.ts +++ b/packages/coding-agent/test/core/python-kernel.lifecycle.test.ts @@ -80,8 +80,8 @@ describe("PythonKernel gateway lifecycle", () => { const originalSleep = Bun.sleep; const originalWhich = Bun.which; const originalExecute = PythonKernel.prototype.execute; - const originalGatewayUrl = process.env.OMP_PYTHON_GATEWAY_URL; - const originalGatewayToken = process.env.OMP_PYTHON_GATEWAY_TOKEN; + const originalGatewayUrl = process.env.PI_PYTHON_GATEWAY_URL; + const originalGatewayToken = process.env.PI_PYTHON_GATEWAY_TOKEN; const originalBunEnv = process.env.BUN_ENV; let tempDir: TempDir; @@ -92,8 +92,8 @@ describe("PythonKernel gateway lifecycle", () => { env = { fetchCalls: [], spawnCalls: [] }; process.env.BUN_ENV = "test"; - delete process.env.OMP_PYTHON_GATEWAY_URL; - delete process.env.OMP_PYTHON_GATEWAY_TOKEN; + delete process.env.PI_PYTHON_GATEWAY_URL; + delete process.env.PI_PYTHON_GATEWAY_TOKEN; FakeWebSocket.instances = []; globalThis.WebSocket = FakeWebSocket as unknown as typeof WebSocket; @@ -130,14 +130,14 @@ describe("PythonKernel gateway lifecycle", () => { process.env.BUN_ENV = originalBunEnv; } if (originalGatewayUrl === undefined) { - delete process.env.OMP_PYTHON_GATEWAY_URL; + delete process.env.PI_PYTHON_GATEWAY_URL; } else { - process.env.OMP_PYTHON_GATEWAY_URL = originalGatewayUrl; + process.env.PI_PYTHON_GATEWAY_URL = originalGatewayUrl; } if (originalGatewayToken === undefined) { - delete process.env.OMP_PYTHON_GATEWAY_TOKEN; + delete process.env.PI_PYTHON_GATEWAY_TOKEN; } else { - process.env.OMP_PYTHON_GATEWAY_TOKEN = originalGatewayToken; + process.env.PI_PYTHON_GATEWAY_TOKEN = originalGatewayToken; } globalThis.fetch = originalFetch; diff --git a/packages/coding-agent/test/core/python-kernel.test.ts b/packages/coding-agent/test/core/python-kernel.test.ts index acad1a857..a2262f793 100644 --- a/packages/coding-agent/test/core/python-kernel.test.ts +++ b/packages/coding-agent/test/core/python-kernel.test.ts @@ -150,8 +150,8 @@ describe("PythonKernel (external gateway)", () => { const originalWebSocket = globalThis.WebSocket; beforeEach(() => { - process.env.OMP_PYTHON_GATEWAY_URL = "http://gateway.test"; - process.env.OMP_PYTHON_SKIP_CHECK = "1"; + process.env.PI_PYTHON_GATEWAY_URL = "http://gateway.test"; + process.env.PI_PYTHON_SKIP_CHECK = "1"; globalThis.WebSocket = FakeWebSocket as unknown as typeof WebSocket; }); diff --git a/packages/coding-agent/test/rpc.test.ts b/packages/coding-agent/test/rpc.test.ts index f4a06b94b..ebf980964 100644 --- a/packages/coding-agent/test/rpc.test.ts +++ b/packages/coding-agent/test/rpc.test.ts @@ -36,7 +36,7 @@ describe.skipIf(!process.env.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_OAUTH_T client = new RpcClient({ cliPath: path.join(import.meta.dir, "..", "dist", "cli.js"), cwd: path.join(import.meta.dir, ".."), - env: { OMP_CODING_AGENT_DIR: sessionDir }, + env: { PI_CODING_AGENT_DIR: sessionDir }, provider: "anthropic", model: "claude-sonnet-4-5", }); diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index 43c1c221e..3c16a3aa8 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -46,8 +46,8 @@ describe("Coding Agent Tools", () => { beforeEach(() => { // Force replace mode for edit tool tests using old_text/new_text - originalEditVariant = process.env.OMP_EDIT_VARIANT; - process.env.OMP_EDIT_VARIANT = "replace"; + originalEditVariant = process.env.PI_EDIT_VARIANT; + process.env.PI_EDIT_VARIANT = "replace"; // Create a unique temporary directory for each test testDir = path.join(os.tmpdir(), `coding-agent-test-${nanoid()}`); @@ -69,9 +69,9 @@ describe("Coding Agent Tools", () => { // Restore original edit variant if (originalEditVariant === undefined) { - delete process.env.OMP_EDIT_VARIANT; + delete process.env.PI_EDIT_VARIANT; } else { - process.env.OMP_EDIT_VARIANT = originalEditVariant; + process.env.PI_EDIT_VARIANT = originalEditVariant; } }); @@ -419,12 +419,12 @@ function b() { }); it("should persist environment variables between commands", async () => { - if (process.platform === "win32" || process.env.OMP_SHELL_PERSIST !== "1") { + if (process.platform === "win32" || process.env.PI_SHELL_PERSIST !== "1") { return; } - await bashTool.execute("test-call-8-env-set", { command: "export OMP_TEST_VAR=hello" }); - const result = await bashTool.execute("test-call-8-env-get", { command: "echo $OMP_TEST_VAR" }); + await bashTool.execute("test-call-8-env-set", { command: "export PI_TEST_VAR=hello" }); + const result = await bashTool.execute("test-call-8-env-get", { command: "echo $PI_TEST_VAR" }); expect(getTextOutput(result)).toContain("hello"); }); @@ -575,8 +575,8 @@ describe("edit tool CRLF handling", () => { beforeEach(() => { // Force replace mode for edit tool tests using old_text/new_text - originalEditVariant = process.env.OMP_EDIT_VARIANT; - process.env.OMP_EDIT_VARIANT = "replace"; + originalEditVariant = process.env.PI_EDIT_VARIANT; + process.env.PI_EDIT_VARIANT = "replace"; testDir = path.join(os.tmpdir(), `coding-agent-crlf-test-${nanoid()}`); fs.mkdirSync(testDir, { recursive: true }); @@ -588,9 +588,9 @@ describe("edit tool CRLF handling", () => { // Restore original edit variant if (originalEditVariant === undefined) { - delete process.env.OMP_EDIT_VARIANT; + delete process.env.PI_EDIT_VARIANT; } else { - process.env.OMP_EDIT_VARIANT = originalEditVariant; + process.env.PI_EDIT_VARIANT = originalEditVariant; } }); diff --git a/packages/coding-agent/test/tools/index.test.ts b/packages/coding-agent/test/tools/index.test.ts index ae4f98847..9c61f5a6a 100644 --- a/packages/coding-agent/test/tools/index.test.ts +++ b/packages/coding-agent/test/tools/index.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "bun:test"; import { type SettingPath, Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import { createTools, HIDDEN_TOOLS, type ToolSession } from "@oh-my-pi/pi-coding-agent/tools"; -process.env.OMP_PYTHON_SKIP_CHECK = "1"; +process.env.PI_PYTHON_SKIP_CHECK = "1"; function createTestSession(overrides: Partial = {}): ToolSession { return { diff --git a/packages/coding-agent/test/tools/python-tool-mode.test.ts b/packages/coding-agent/test/tools/python-tool-mode.test.ts index 6489262d1..75b467c1b 100644 --- a/packages/coding-agent/test/tools/python-tool-mode.test.ts +++ b/packages/coding-agent/test/tools/python-tool-mode.test.ts @@ -19,8 +19,8 @@ function createSession(overrides: Partial = {}): ToolSession { describe("createTools python fallback", () => { it("falls back to bash when python is requested but disabled", async () => { - const previous = process.env.OMP_PYTHON_SKIP_CHECK; - process.env.OMP_PYTHON_SKIP_CHECK = "1"; + const previous = process.env.PI_PYTHON_SKIP_CHECK; + process.env.PI_PYTHON_SKIP_CHECK = "1"; const session = createSession(); const tools = await createTools(session, ["python"]); const names = tools.map(tool => tool.name).sort(); @@ -28,9 +28,9 @@ describe("createTools python fallback", () => { expect(names).toEqual(["bash", "exit_plan_mode"]); if (previous === undefined) { - delete process.env.OMP_PYTHON_SKIP_CHECK; + delete process.env.PI_PYTHON_SKIP_CHECK; } else { - process.env.OMP_PYTHON_SKIP_CHECK = previous; + process.env.PI_PYTHON_SKIP_CHECK = previous; } }); }); diff --git a/packages/coding-agent/test/tools/python.test.ts b/packages/coding-agent/test/tools/python.test.ts index 1bf450258..c8b1db84b 100644 --- a/packages/coding-agent/test/tools/python.test.ts +++ b/packages/coding-agent/test/tools/python.test.ts @@ -9,16 +9,16 @@ let previousSkipCheck: string | undefined; let tempDir: TempDir; beforeAll(() => { tempDir = TempDir.createSync("@omp-python-test-"); - previousSkipCheck = process.env.OMP_PYTHON_SKIP_CHECK; - process.env.OMP_PYTHON_SKIP_CHECK = "1"; + previousSkipCheck = process.env.PI_PYTHON_SKIP_CHECK; + process.env.PI_PYTHON_SKIP_CHECK = "1"; }); afterAll(() => { if (previousSkipCheck === undefined) { - delete process.env.OMP_PYTHON_SKIP_CHECK; + delete process.env.PI_PYTHON_SKIP_CHECK; return; } - process.env.OMP_PYTHON_SKIP_CHECK = previousSkipCheck; + process.env.PI_PYTHON_SKIP_CHECK = previousSkipCheck; tempDir.removeSync(); }); diff --git a/packages/natives/src/native.ts b/packages/natives/src/native.ts index d29efde8b..9b95402a5 100644 --- a/packages/natives/src/native.ts +++ b/packages/natives/src/native.ts @@ -8,7 +8,9 @@ import * as fs from "node:fs"; import { createRequire } from "node:module"; import * as os from "node:os"; import * as path from "node:path"; -import packageJson from "../package.json" with { type: "json" }; +import { getEnv } from "@oh-my-pi/pi-utils"; + +import packageJson from "../package.json"; import type { NativeBindings } from "./bindings"; import { embeddedAddon } from "./embedded-addon"; @@ -64,7 +66,7 @@ const compiledCandidates = [ ]; const releaseCandidates = isCompiledBinary ? [...compiledCandidates, ...baseReleaseCandidates] : baseReleaseCandidates; -const candidates = process.env.OMP_DEV ? [...debugCandidates, ...releaseCandidates] : releaseCandidates; +const candidates = getEnv("PI_DEV") ? [...debugCandidates, ...releaseCandidates] : releaseCandidates; function maybeExtractEmbeddedAddon(errors: string[]): string | null { if (!isCompiledBinary || !embeddedAddon) return null; @@ -109,12 +111,12 @@ function loadNative(): NativeBindings { try { const bindings = require(candidate) as NativeBindings; validateNative(bindings, candidate); - if (process.env.OMP_DEV) { + if (getEnv("PI_DEV")) { console.log(`Loaded native addon from ${candidate}`); } return bindings; } catch (err) { - if (process.env.OMP_DEV) { + if (getEnv("PI_DEV")) { console.error(`Error loading native addon from ${candidate}:`, err); } const message = err instanceof Error ? err.message : String(err); diff --git a/packages/react-edit-benchmark/runner.ts b/packages/react-edit-benchmark/runner.ts index c2816eefe..b1dbfc589 100644 --- a/packages/react-edit-benchmark/runner.ts +++ b/packages/react-edit-benchmark/runner.ts @@ -196,15 +196,15 @@ async function runSingleTask( try { await fs.appendFile(logFile, `{"type":"meta","task":"${task.id}","run":${runIndex},"workDir":"${cwd}"}\n`); - const env: Record = { OMP_NO_TITLE: "1" }; + const env: Record = { PI_NO_TITLE: "1" }; if (config.editVariant !== undefined) { - env.OMP_EDIT_VARIANT = config.editVariant; + env.PI_EDIT_VARIANT = config.editVariant; } if (config.editFuzzy !== undefined) { - env.OMP_EDIT_FUZZY = config.editFuzzy === "auto" ? "auto" : config.editFuzzy ? "1" : "0"; + env.PI_EDIT_FUZZY = config.editFuzzy === "auto" ? "auto" : config.editFuzzy ? "1" : "0"; } if (config.editFuzzyThreshold !== undefined) { - env.OMP_EDIT_FUZZY_THRESHOLD = + env.PI_EDIT_FUZZY_THRESHOLD = config.editFuzzyThreshold === "auto" ? "auto" : String(config.editFuzzyThreshold); } @@ -765,15 +765,15 @@ async function runBatch( await Promise.all(orderedItems.map((item) => copyFixtures(item.task, workDir))); - const env: Record = { OMP_NO_TITLE: "1" }; + const env: Record = { PI_NO_TITLE: "1" }; if (config.editVariant !== undefined) { - env.OMP_EDIT_VARIANT = config.editVariant; + env.PI_EDIT_VARIANT = config.editVariant; } if (config.editFuzzy !== undefined) { - env.OMP_EDIT_FUZZY = config.editFuzzy === "auto" ? "auto" : config.editFuzzy ? "1" : "0"; + env.PI_EDIT_FUZZY = config.editFuzzy === "auto" ? "auto" : config.editFuzzy ? "1" : "0"; } if (config.editFuzzyThreshold !== undefined) { - env.OMP_EDIT_FUZZY_THRESHOLD = + env.PI_EDIT_FUZZY_THRESHOLD = config.editFuzzyThreshold === "auto" ? "auto" : String(config.editFuzzyThreshold); } diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 538ff90ea..e1bf70f72 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] + ### Added - Introduced `terminal-capabilities.ts` module consolidating terminal detection and image protocol support @@ -11,6 +12,10 @@ ### Changed +- Changed notification suppression environment variable from `OMP_NOTIFICATIONS` to `PI_NOTIFICATIONS` +- Changed TUI write log environment variable from `OMP_TUI_WRITE_LOG` to `PI_TUI_WRITE_LOG` +- Changed hardware cursor environment variable from `OMP_HARDWARE_CURSOR` to `PI_HARDWARE_CURSOR` +- Updated environment variable access to use `getEnv()` utility function from `@oh-my-pi/pi-utils` for consistent handling - Renamed `TERMINAL_INFO` export to `TERMINAL` for clearer API semantics - Reorganized terminal image exports from `terminal-image` to `terminal-capabilities` module - Updated all internal references to use `TERMINAL` instead of `TERMINAL_INFO` diff --git a/packages/tui/package.json b/packages/tui/package.json index 78fe29982..42a11ed21 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -48,6 +48,7 @@ }, "dependencies": { "@oh-my-pi/pi-natives": "workspace:*", + "@oh-my-pi/pi-utils": "workspace:*", "@types/mime-types": "^3.0.1", "chalk": "^5.6.2", "marked": "^17.0.1", diff --git a/packages/tui/src/terminal-capabilities.ts b/packages/tui/src/terminal-capabilities.ts index e6b8b320a..0b35510ed 100644 --- a/packages/tui/src/terminal-capabilities.ts +++ b/packages/tui/src/terminal-capabilities.ts @@ -42,7 +42,7 @@ export class TerminalInfo { } export function isNotificationSuppressed(): boolean { - const value = getEnv("OMP_NOTIFICATIONS"); + const value = getEnv("PI_NOTIFICATIONS"); if (!value) return false; return value === "off" || value === "0" || value === "false"; } diff --git a/packages/tui/src/terminal.ts b/packages/tui/src/terminal.ts index 86fb870f0..c1742f415 100644 --- a/packages/tui/src/terminal.ts +++ b/packages/tui/src/terminal.ts @@ -1,5 +1,5 @@ import * as fs from "node:fs"; -import { logger } from "@oh-my-pi/pi-utils"; +import { getEnv, logger } from "@oh-my-pi/pi-utils"; import { setKittyProtocolActive } from "./keys"; import { StdinBuffer } from "./stdin-buffer"; @@ -82,7 +82,7 @@ export class ProcessTerminal implements Terminal { private stdinBuffer?: StdinBuffer; private stdinDataHandler?: (data: string) => void; private dead = false; - private writeLogPath = process.env.OMP_TUI_WRITE_LOG || ""; + private writeLogPath = getEnv("PI_TUI_WRITE_LOG") || ""; get kittyProtocolActive(): boolean { return this._kittyProtocolActive; diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index 685efd8ea..fb9e91e74 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -4,6 +4,7 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; +import { getEnv } from "@oh-my-pi/pi-utils"; import { isKeyRelease, matchesKey } from "./keys"; import type { Terminal } from "./terminal"; import { setCellDimensions, TERMINAL } from "./terminal-capabilities"; @@ -227,7 +228,7 @@ export class TUI extends Container { private hardwareCursorRow = 0; // Actual terminal cursor row (may differ due to IME positioning) private inputBuffer = ""; // Buffer for parsing terminal responses private cellSizeQueryPending = false; - private showHardwareCursor = process.env.OMP_HARDWARE_CURSOR === "1"; + private showHardwareCursor = getEnv("PI_HARDWARE_CURSOR") === "1"; private maxLinesRendered = 0; // Track terminal's working area (max lines ever rendered) private previousViewportTop = 0; // Track previous viewport top for resize-aware cursor moves private fullRedrawCount = 0; diff --git a/packages/utils/src/env.ts b/packages/utils/src/env.ts index 70d8d6b1b..73b38d041 100644 --- a/packages/utils/src/env.ts +++ b/packages/utils/src/env.ts @@ -33,6 +33,14 @@ function parseEnvFile(filePath: string): Record { } catch { // File doesn't exist or can't be read - return empty result } + + // OMP_ overrides PI_ + for (const k in result) { + if (k.startsWith("OMP_")) { + result[`PI_${k.slice(4)}`] = result[k]; + } + } + return result; } diff --git a/packages/utils/src/procmgr.ts b/packages/utils/src/procmgr.ts index e42e5d721..6ac9d2ad4 100644 --- a/packages/utils/src/procmgr.ts +++ b/packages/utils/src/procmgr.ts @@ -2,6 +2,7 @@ import * as fs from "node:fs"; import path from "node:path"; import * as timers from "node:timers"; import type { Subprocess } from "bun"; +import { getEnv } from "./env"; export interface ShellConfig { shell: string; @@ -31,7 +32,7 @@ function isExecutable(path: string): boolean { * Build the spawn environment (cached). */ function buildSpawnEnv(shell: string): Record { - const noCI = process.env.OMP_BASH_NO_CI || process.env.CLAUDE_BASH_NO_CI; + const noCI = getEnv("PI_BASH_NO_CI") || getEnv("CLAUDE_BASH_NO_CI"); return { ...process.env, SHELL: shell, @@ -45,18 +46,18 @@ function buildSpawnEnv(shell: string): Record { /** * Get shell args, optionally including login shell flag. - * Supports OMP_BASH_NO_LOGIN and CLAUDE_BASH_NO_LOGIN to skip -l. + * Supports PI_BASH_NO_LOGIN and CLAUDE_BASH_NO_LOGIN to skip -l. */ function getShellArgs(): string[] { - const noLogin = process.env.OMP_BASH_NO_LOGIN || process.env.CLAUDE_BASH_NO_LOGIN; - return noLogin ? ["-c"] : ["-l", "-c"]; + const noLogin = getEnv("PI_BASH_NO_LOGIN") || getEnv("CLAUDE_BASH_NO_LOGIN"); + return noLogin ? ["-l", "-c"] : ["-l", "-c"]; } /** * Get shell prefix for wrapping commands (profilers, strace, etc.). */ function getShellPrefix(): string | undefined { - return process.env.OMP_SHELL_PREFIX || process.env.CLAUDE_CODE_SHELL_PREFIX; + return getEnv("PI_SHELL_PREFIX") || getEnv("CLAUDE_CODE_SHELL_PREFIX"); } /** diff --git a/scripts/install.ps1 b/scripts/install.ps1 index e63a0a175..b1c1cf71f 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -18,7 +18,7 @@ $ErrorActionPreference = "Stop" $Repo = "can1357/oh-my-pi" $Package = "@oh-my-pi/pi-coding-agent" -$InstallDir = if ($env:OMP_INSTALL_DIR) { $env:OMP_INSTALL_DIR } else { "$env:LOCALAPPDATA\omp" } +$InstallDir = if ($env:PI_INSTALL_DIR) { $env:PI_INSTALL_DIR } else { "$env:LOCALAPPDATA\omp" } $BinaryName = "omp-windows-x64.exe" $NativeAddonName = "pi_natives.win32-x64.node" $MinimumBunVersion = "1.3.7" diff --git a/scripts/install.sh b/scripts/install.sh index 73a168d08..4f06aa7e9 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -12,7 +12,7 @@ set -e REPO="can1357/oh-my-pi" PACKAGE="@oh-my-pi/pi-coding-agent" -INSTALL_DIR="${OMP_INSTALL_DIR:-$HOME/.local/bin}" +INSTALL_DIR="${PI_INSTALL_DIR:-$HOME/.local/bin}" MIN_BUN_VERSION="1.3.7" # Parse arguments diff --git a/scripts/repro-stuck.ts b/scripts/repro-stuck.ts index d5f35c779..0237469de 100644 --- a/scripts/repro-stuck.ts +++ b/scripts/repro-stuck.ts @@ -77,7 +77,7 @@ async function spawnBatch(count: number, basePort: number, timeout: number): Pro stdout: "pipe", stderr: "pipe", stdin: "pipe", - env: { ...process.env, NO_COLOR: "1", OMP_DEBUG_STARTUP: "1" }, + env: { ...process.env, NO_COLOR: "1", PI_DEBUG_STARTUP: "1" }, }); instances.push({ proc, port, stdout: "", stderr: "", status: "pending" }); }