@oh-my-pi/pi-coding-agent 12.1.0 → 12.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/examples/sdk/11-sessions.ts +1 -1
  3. package/package.json +16 -11
  4. package/src/capability/index.ts +2 -1
  5. package/src/capability/types.ts +1 -1
  6. package/src/cli/file-processor.ts +2 -1
  7. package/src/cli/shell-cli.ts +2 -2
  8. package/src/commit/agentic/index.ts +2 -1
  9. package/src/commit/pipeline.ts +2 -1
  10. package/src/config/prompt-templates.ts +3 -3
  11. package/src/config/settings-schema.ts +11 -0
  12. package/src/config/settings.ts +2 -2
  13. package/src/config.ts +6 -6
  14. package/src/debug/system-info.ts +2 -2
  15. package/src/extensibility/custom-commands/loader.ts +5 -5
  16. package/src/extensibility/plugins/installer.ts +2 -2
  17. package/src/extensibility/plugins/manager.ts +2 -1
  18. package/src/extensibility/skills.ts +3 -2
  19. package/src/extensibility/slash-commands.ts +1 -1
  20. package/src/ipy/executor.ts +2 -2
  21. package/src/ipy/modules.ts +3 -3
  22. package/src/main.ts +9 -7
  23. package/src/mcp/transports/stdio.ts +2 -1
  24. package/src/modes/components/footer.ts +3 -2
  25. package/src/modes/components/oauth-selector.ts +96 -21
  26. package/src/modes/components/status-line/segments.ts +2 -1
  27. package/src/modes/components/status-line.ts +2 -1
  28. package/src/modes/components/tool-execution.ts +2 -1
  29. package/src/modes/controllers/command-controller.ts +60 -2
  30. package/src/modes/controllers/mcp-command-controller.ts +8 -8
  31. package/src/modes/controllers/selector-controller.ts +35 -11
  32. package/src/modes/interactive-mode.ts +2 -2
  33. package/src/sdk.ts +37 -11
  34. package/src/session/agent-session.ts +12 -0
  35. package/src/session/session-manager.ts +7 -4
  36. package/src/system-prompt.ts +41 -28
  37. package/src/tools/bash-normalize.ts +2 -2
  38. package/src/tools/bash.ts +2 -1
  39. package/src/tools/fetch.ts +3 -3
  40. package/src/tools/python.ts +2 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,36 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [12.2.0] - 2026-02-13
6
+
7
+ ### Added
8
+
9
+ - Added `providerSessionState` property to AgentSession for managing provider-scoped transport and session caches
10
+ - Added automatic cleanup of provider session state resources on session disposal
11
+ - Added `providers.openaiWebsockets` setting to prefer websocket transport for OpenAI Codex models
12
+ - Added provider details display in session info showing authentication mode, transport, and connection settings
13
+ - Added automatic prewarm of OpenAI Codex websocket connections on session creation for improved performance
14
+ - Added real-time authentication validation in OAuth provider selector with visual status indicators (checking, valid, invalid)
15
+ - Added `validateAuth` and `requestRender` options to OAuthSelectorComponent for custom authentication validation and UI refresh callbacks
16
+
17
+ ### Changed
18
+
19
+ - Changed `providers.openaiWebsockets` setting from boolean to enum with values "auto", "off", "on" for more granular websocket policy control (auto uses model defaults, on forces websocket, off disables it)
20
+ - Enhanced provider details display to include live provider session state information
21
+ - Enhanced session info output to display active provider configuration and authentication details
22
+ - Replaced `process.cwd()` with `getProjectDir()` throughout codebase for improved project directory detection and handling
23
+ - Made `SessionManager.list()` async to support asynchronous session discovery operations
24
+ - Preserved internal whitespace and indentation in bash command normalization to support heredocs and indentation-sensitive scripts
25
+ - Improved git context loading performance with configurable timeouts and parallel status/commit queries
26
+ - Enhanced git context reliability with better error handling for timeout and command failures
27
+ - Changed OAuth provider selector to display live authentication status instead of static login state
28
+ - Changed logout flow to refresh OAuth provider authentication state before showing selector
29
+
30
+ ### Fixed
31
+
32
+ - Improved error reporting in fetch tool to include HTTP status codes when URL fetching fails
33
+ - Fixed fetch tool to preserve actual response metadata (finalUrl, contentType) instead of defaults when requests fail
34
+
5
35
  ## [12.1.0] - 2026-02-13
6
36
 
7
37
  ### Added
@@ -25,7 +25,7 @@ if (modelFallbackMessage) console.log("Note:", modelFallbackMessage);
25
25
  console.log("Continued session:", continued.sessionFile);
26
26
 
27
27
  // List and open specific session
28
- const sessions = SessionManager.list(process.cwd());
28
+ const sessions = await SessionManager.list(process.cwd());
29
29
  console.log(`\nFound ${sessions.length} sessions:`);
30
30
  for (const info of sessions.slice(0, 3)) {
31
31
  console.log(` ${info.id.slice(0, 8)}… - "${info.firstMessage.slice(0, 30)}…"`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "12.1.0",
3
+ "version": "12.2.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "bin": {
@@ -84,12 +84,12 @@
84
84
  },
85
85
  "dependencies": {
86
86
  "@mozilla/readability": "0.6.0",
87
- "@oh-my-pi/omp-stats": "12.1.0",
88
- "@oh-my-pi/pi-agent-core": "12.1.0",
89
- "@oh-my-pi/pi-ai": "12.1.0",
90
- "@oh-my-pi/pi-natives": "12.1.0",
91
- "@oh-my-pi/pi-tui": "12.1.0",
92
- "@oh-my-pi/pi-utils": "12.1.0",
87
+ "@oh-my-pi/omp-stats": "12.2.0",
88
+ "@oh-my-pi/pi-agent-core": "12.2.0",
89
+ "@oh-my-pi/pi-ai": "12.2.0",
90
+ "@oh-my-pi/pi-natives": "12.2.0",
91
+ "@oh-my-pi/pi-tui": "12.2.0",
92
+ "@oh-my-pi/pi-utils": "12.2.0",
93
93
  "@sinclair/typebox": "^0.34.48",
94
94
  "@xterm/headless": "^6.0.0",
95
95
  "ajv": "^8.17.1",
@@ -119,14 +119,19 @@
119
119
  "tui",
120
120
  "agent"
121
121
  ],
122
- "author": "Mario Zechner",
122
+ "author": "Can Bölük",
123
+ "contributors": ["Mario Zechner"],
123
124
  "license": "MIT",
124
125
  "repository": {
125
126
  "type": "git",
126
127
  "url": "git+https://github.com/can1357/oh-my-pi.git",
127
128
  "directory": "packages/coding-agent"
128
129
  },
129
- "engines": {
130
- "bun": ">=1.3.7"
131
- }
130
+ "homepage": "https://github.com/can1357/oh-my-pi",
131
+ "bugs": {
132
+ "url": "https://github.com/can1357/oh-my-pi/issues"
133
+ },
134
+ "engines": {
135
+ "bun": ">=1.3.7"
136
+ }
132
137
  }
@@ -9,6 +9,7 @@
9
9
  import * as os from "node:os";
10
10
  import * as path from "node:path";
11
11
  import { $env } from "@oh-my-pi/pi-utils";
12
+ import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
12
13
 
13
14
  /** Conditional startup debug prints (stderr) when PI_DEBUG_STARTUP is set */
14
15
  const debugStartup = $env.PI_DEBUG_STARTUP ? (stage: string) => process.stderr.write(`[startup] ${stage}\n`) : () => {};
@@ -221,7 +222,7 @@ export async function loadCapability<T>(capabilityId: string, options: LoadOptio
221
222
  throw new Error(`Unknown capability: "${capabilityId}"`);
222
223
  }
223
224
 
224
- const cwd = options.cwd ?? process.cwd();
225
+ const cwd = options.cwd ?? getProjectDir();
225
226
  const home = os.homedir();
226
227
  const ctx: LoadContext = { cwd, home };
227
228
  const providers = filterProviders(capability, options);
@@ -62,7 +62,7 @@ export interface LoadOptions {
62
62
  providers?: string[];
63
63
  /** Exclude these providers (by ID). Default: none */
64
64
  excludeProviders?: string[];
65
- /** Custom cwd. Default: process.cwd() */
65
+ /** Custom cwd. Default: getProjectDir() */
66
66
  cwd?: string;
67
67
  /** Include items even if they fail validation. Default: false */
68
68
  includeInvalid?: boolean;
@@ -5,6 +5,7 @@ import * as fs from "node:fs";
5
5
  import * as path from "node:path";
6
6
  import type { ImageContent } from "@oh-my-pi/pi-ai";
7
7
  import { isEnoent } from "@oh-my-pi/pi-utils";
8
+ import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
8
9
  import chalk from "chalk";
9
10
  import { resolveReadPath } from "../tools/path-utils";
10
11
  import { formatSize } from "../tools/truncate";
@@ -34,7 +35,7 @@ export async function processFileArguments(fileArgs: string[], options?: Process
34
35
 
35
36
  for (const fileArg of fileArgs) {
36
37
  // Expand and resolve path (handles ~ expansion and macOS screenshot Unicode spaces)
37
- const absolutePath = path.resolve(resolveReadPath(fileArg, process.cwd()));
38
+ const absolutePath = path.resolve(resolveReadPath(fileArg, getProjectDir()));
38
39
 
39
40
  const stat = fs.statSync(absolutePath, { throwIfNoEntry: false });
40
41
  if (!stat) {
@@ -6,7 +6,7 @@
6
6
  import * as path from "node:path";
7
7
  import { createInterface } from "node:readline/promises";
8
8
  import { Shell } from "@oh-my-pi/pi-natives";
9
- import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
9
+ import { APP_NAME, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
10
10
  import chalk from "chalk";
11
11
  import { Settings } from "../config/settings";
12
12
  import { getOrCreateSnapshot } from "../utils/shell-snapshot";
@@ -47,7 +47,7 @@ export async function runShellCommand(cmd: ShellCommandArgs): Promise<void> {
47
47
  process.exit(1);
48
48
  }
49
49
 
50
- const cwd = cmd.cwd ? path.resolve(cmd.cwd) : process.cwd();
50
+ const cwd = cmd.cwd ? path.resolve(cmd.cwd) : getProjectDir();
51
51
  const settings = await Settings.init({ cwd });
52
52
  const { shell, env: shellEnv } = settings.getShellConfig();
53
53
  const snapshotPath = cmd.noSnapshot || !shell.includes("bash") ? null : await getOrCreateSnapshot(shell, shellEnv);
@@ -1,6 +1,7 @@
1
1
  import * as path from "node:path";
2
2
  import { createInterface } from "node:readline/promises";
3
3
  import { $env } from "@oh-my-pi/pi-utils";
4
+ import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
4
5
  import { applyChangelogProposals } from "../../commit/changelog";
5
6
  import { detectChangelogBoundaries } from "../../commit/changelog/detect";
6
7
  import { parseUnreleasedSection } from "../../commit/changelog/parse";
@@ -26,7 +27,7 @@ interface CommitExecutionContext {
26
27
  }
27
28
 
28
29
  export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
29
- const cwd = process.cwd();
30
+ const cwd = getProjectDir();
30
31
  const git = new ControlledGit(cwd);
31
32
  const [settings, authStorage] = await Promise.all([Settings.init({ cwd }), discoverAuthStorage()]);
32
33
 
@@ -1,6 +1,7 @@
1
1
  import * as path from "node:path";
2
2
  import type { Api, Model } from "@oh-my-pi/pi-ai";
3
3
  import { logger } from "@oh-my-pi/pi-utils";
4
+ import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
4
5
  import { ModelRegistry } from "../config/model-registry";
5
6
  import { renderPromptTemplate } from "../config/prompt-templates";
6
7
  import { Settings } from "../config/settings";
@@ -38,7 +39,7 @@ export async function runCommitCommand(args: CommitCommandArgs): Promise<void> {
38
39
  }
39
40
 
40
41
  async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
41
- const cwd = process.cwd();
42
+ const cwd = getProjectDir();
42
43
  const settings = await Settings.init();
43
44
  const commitSettings = settings.getGroup("commit");
44
45
  const authStorage = await discoverAuthStorage();
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { logger } from "@oh-my-pi/pi-utils";
4
- import { getProjectPromptsDir, getPromptsDir } from "@oh-my-pi/pi-utils/dirs";
4
+ import { getProjectDir, getProjectPromptsDir, getPromptsDir } from "@oh-my-pi/pi-utils/dirs";
5
5
  import Handlebars from "handlebars";
6
6
  import { computeLineHash } from "../patch/hashline";
7
7
  import { jtdToTypeScript } from "../tools/jtd-to-typescript";
@@ -459,7 +459,7 @@ async function loadTemplatesFromDir(
459
459
  }
460
460
 
461
461
  export interface LoadPromptTemplatesOptions {
462
- /** Working directory for project-local templates. Default: process.cwd() */
462
+ /** Working directory for project-local templates. Default: getProjectDir() */
463
463
  cwd?: string;
464
464
  /** Agent config directory for global templates. Default: from getPromptsDir() */
465
465
  agentDir?: string;
@@ -471,7 +471,7 @@ export interface LoadPromptTemplatesOptions {
471
471
  * 2. Project: cwd/.omp/prompts/
472
472
  */
473
473
  export async function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): Promise<PromptTemplate[]> {
474
- const resolvedCwd = options.cwd ?? process.cwd();
474
+ const resolvedCwd = options.cwd ?? getProjectDir();
475
475
  const resolvedAgentDir = options.agentDir ?? getPromptsDir();
476
476
 
477
477
  const templates: PromptTemplate[] = [];
@@ -592,6 +592,17 @@ export const SETTINGS_SCHEMA = {
592
592
  submenu: true,
593
593
  },
594
594
  },
595
+ "providers.openaiWebsockets": {
596
+ type: "enum",
597
+ values: ["auto", "off", "on"] as const,
598
+ default: "auto",
599
+ ui: {
600
+ tab: "services",
601
+ label: "OpenAI websockets",
602
+ description: "Websocket policy for OpenAI Codex models (auto uses model defaults, on forces, off disables)",
603
+ submenu: true,
604
+ },
605
+ },
595
606
 
596
607
  // ─────────────────────────────────────────────────────────────────────────
597
608
  // Exa settings
@@ -14,7 +14,7 @@
14
14
  import * as fs from "node:fs";
15
15
  import * as path from "node:path";
16
16
  import { isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
17
- import { getAgentDbPath, getAgentDir } from "@oh-my-pi/pi-utils/dirs";
17
+ import { getAgentDbPath, getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
18
18
  import { YAML } from "bun";
19
19
  import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings";
20
20
  import type { ModelRole } from "../config/model-registry";
@@ -146,7 +146,7 @@ export class Settings {
146
146
  #persist: boolean;
147
147
 
148
148
  private constructor(options: SettingsOptions = {}) {
149
- this.#cwd = path.normalize(options.cwd ?? process.cwd());
149
+ this.#cwd = path.normalize(options.cwd ?? getProjectDir());
150
150
  this.#agentDir = path.normalize(options.agentDir ?? getAgentDir());
151
151
  this.#configPath = options.inMemory ? null : path.join(this.#agentDir, "config.yml");
152
152
  this.#persist = !options.inMemory;
package/src/config.ts CHANGED
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { isEnoent, logger } from "@oh-my-pi/pi-utils";
5
- import { CONFIG_DIR_NAME, getAgentDir } from "@oh-my-pi/pi-utils/dirs";
5
+ import { CONFIG_DIR_NAME, getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
6
6
  import type { TSchema } from "@sinclair/typebox";
7
7
  import { Value } from "@sinclair/typebox/value";
8
8
  import { Ajv, type ErrorObject, type ValidateFunction } from "ajv";
@@ -39,8 +39,8 @@ export function getPackageDir(): string {
39
39
  }
40
40
  dir = path.dirname(dir);
41
41
  }
42
- // Fallback to cwd (docs/examples won't be found, but that's fine)
43
- return process.cwd();
42
+ // Fallback to project dir (docs/examples won't be found, but that's fine)
43
+ return getProjectDir();
44
44
  }
45
45
 
46
46
  /** Get path to CHANGELOG.md (optional, may not exist in binary) */
@@ -273,7 +273,7 @@ export interface GetConfigDirsOptions {
273
273
  user?: boolean;
274
274
  /** Include project-level directories (.omp/...). Default: true */
275
275
  project?: boolean;
276
- /** Current working directory for project paths. Default: process.cwd() */
276
+ /** Current working directory for project paths. Default: getProjectDir() */
277
277
  cwd?: string;
278
278
  /** Only return directories that exist. Default: false */
279
279
  existingOnly?: boolean;
@@ -296,7 +296,7 @@ export interface GetConfigDirsOptions {
296
296
  * getConfigDirs("skills", { user: false, existingOnly: true })
297
297
  */
298
298
  export function getConfigDirs(subpath: string, options: GetConfigDirsOptions = {}): ConfigDirEntry[] {
299
- const { user = true, project = true, cwd = process.cwd(), existingOnly = false } = options;
299
+ const { user = true, project = true, cwd = getProjectDir(), existingOnly = false } = options;
300
300
  const results: ConfigDirEntry[] = [];
301
301
 
302
302
  // User-level directories (highest priority)
@@ -382,7 +382,7 @@ export function findConfigFileWithMeta(
382
382
  * Returns one entry per config base (.omp, .claude) - the nearest one found.
383
383
  * Results are in priority order (highest first).
384
384
  */
385
- export function findAllNearestProjectConfigDirs(subpath: string, cwd: string = process.cwd()): ConfigDirEntry[] {
385
+ export function findAllNearestProjectConfigDirs(subpath: string, cwd: string = getProjectDir()): ConfigDirEntry[] {
386
386
  const results: ConfigDirEntry[] = [];
387
387
  const foundBases = new Set<string>();
388
388
 
@@ -2,7 +2,7 @@
2
2
  * System information collection for debug reports.
3
3
  */
4
4
  import * as os from "node:os";
5
- import { VERSION } from "@oh-my-pi/pi-utils/dirs";
5
+ import { getProjectDir, VERSION } from "@oh-my-pi/pi-utils/dirs";
6
6
 
7
7
  export interface SystemInfo {
8
8
  os: string;
@@ -65,7 +65,7 @@ export async function collectSystemInfo(): Promise<SystemInfo> {
65
65
  bun: Bun.version,
66
66
  node: process.version,
67
67
  },
68
- cwd: process.cwd(),
68
+ cwd: getProjectDir(),
69
69
  shell,
70
70
  terminal,
71
71
  };
@@ -8,7 +8,7 @@ import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
9
  import * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
10
10
  import { isEnoent, logger } from "@oh-my-pi/pi-utils";
11
- import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
11
+ import { getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
12
12
  import * as typebox from "@sinclair/typebox";
13
13
  import { getConfigDirs } from "../../config";
14
14
  import { execCommand } from "../../exec/exec";
@@ -62,7 +62,7 @@ async function loadCommandModule(
62
62
  }
63
63
 
64
64
  export interface DiscoverCustomCommandsOptions {
65
- /** Current working directory. Default: process.cwd() */
65
+ /** Current working directory. Default: getProjectDir() */
66
66
  cwd?: string;
67
67
  /** Agent config directory. Default: from getAgentDir() */
68
68
  agentDir?: string;
@@ -80,7 +80,7 @@ export interface DiscoverCustomCommandsResult {
80
80
  export async function discoverCustomCommands(
81
81
  options: DiscoverCustomCommandsOptions = {},
82
82
  ): Promise<DiscoverCustomCommandsResult> {
83
- const cwd = options.cwd ?? process.cwd();
83
+ const cwd = options.cwd ?? getProjectDir();
84
84
  const agentDir = options.agentDir ?? getAgentDir();
85
85
  const paths: Array<{ path: string; source: CustomCommandSource }> = [];
86
86
  const seen = new Set<string>();
@@ -136,7 +136,7 @@ export async function discoverCustomCommands(
136
136
  }
137
137
 
138
138
  export interface LoadCustomCommandsOptions {
139
- /** Current working directory. Default: process.cwd() */
139
+ /** Current working directory. Default: getProjectDir() */
140
140
  cwd?: string;
141
141
  /** Agent config directory. Default: from getAgentDir() */
142
142
  agentDir?: string;
@@ -163,7 +163,7 @@ function loadBundledCommands(sharedApi: CustomCommandAPI): LoadedCustomCommand[]
163
163
  * Discover and load custom commands from standard locations.
164
164
  */
165
165
  export async function loadCustomCommands(options: LoadCustomCommandsOptions = {}): Promise<CustomCommandsLoadResult> {
166
- const cwd = options.cwd ?? process.cwd();
166
+ const cwd = options.cwd ?? getProjectDir();
167
167
  const agentDir = options.agentDir ?? getAgentDir();
168
168
 
169
169
  const { paths } = await discoverCustomCommands({ cwd, agentDir });
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { isEnoent } from "@oh-my-pi/pi-utils";
4
- import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
4
+ import { getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
5
5
  import type { InstalledPlugin } from "./types";
6
6
 
7
7
  const PLUGINS_DIR = path.join(getAgentDir(), "plugins");
@@ -131,7 +131,7 @@ export async function listPlugins(): Promise<InstalledPlugin[]> {
131
131
  }
132
132
 
133
133
  export async function linkPlugin(localPath: string): Promise<void> {
134
- const cwd = process.cwd();
134
+ const cwd = getProjectDir();
135
135
  const absolutePath = path.resolve(cwd, localPath);
136
136
 
137
137
  // Validate that resolved path is within cwd to prevent path traversal
@@ -6,6 +6,7 @@ import {
6
6
  getPluginsLockfile,
7
7
  getPluginsNodeModules,
8
8
  getPluginsPackageJson,
9
+ getProjectDir,
9
10
  getProjectPluginOverridesPath,
10
11
  } from "@oh-my-pi/pi-utils/dirs";
11
12
  import { extractPackageName, parsePluginSpec } from "./parser";
@@ -50,7 +51,7 @@ export class PluginManager {
50
51
  #runtimeConfig: PluginRuntimeConfig | null = null;
51
52
  #cwd: string;
52
53
 
53
- constructor(cwd: string = process.cwd()) {
54
+ constructor(cwd: string = getProjectDir()) {
54
55
  this.#cwd = cwd;
55
56
  }
56
57
 
@@ -2,6 +2,7 @@ import * as fs from "node:fs/promises";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { logger } from "@oh-my-pi/pi-utils";
5
+ import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
5
6
  import { skillCapability } from "../capability/skill";
6
7
  import type { SourceMeta } from "../capability/types";
7
8
  import type { SkillsSettings } from "../config/settings";
@@ -207,7 +208,7 @@ async function scanDirectoryForSkills(dir: string): Promise<LoadSkillsResult> {
207
208
  }
208
209
 
209
210
  export interface LoadSkillsOptions extends SkillsSettings {
210
- /** Working directory for project-local skills. Default: process.cwd() */
211
+ /** Working directory for project-local skills. Default: getProjectDir() */
211
212
  cwd?: string;
212
213
  }
213
214
 
@@ -217,7 +218,7 @@ export interface LoadSkillsOptions extends SkillsSettings {
217
218
  */
218
219
  export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadSkillsResult> {
219
220
  const {
220
- cwd = process.cwd(),
221
+ cwd = getProjectDir(),
221
222
  enabled = true,
222
223
  enableCodexUser = true,
223
224
  enableClaudeUser = true,
@@ -281,7 +281,7 @@ export function substituteArgs(content: string, args: string[]): string {
281
281
  }
282
282
 
283
283
  export interface LoadSlashCommandsOptions {
284
- /** Working directory for project-local commands. Default: process.cwd() */
284
+ /** Working directory for project-local commands. Default: getProjectDir() */
285
285
  cwd?: string;
286
286
  }
287
287
 
@@ -1,6 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import { $env, isEnoent, logger } from "@oh-my-pi/pi-utils";
3
- import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
3
+ import { getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
4
4
  import { OutputSink } from "../session/streaming-output";
5
5
  import { time } from "../utils/timings";
6
6
  import { shutdownSharedGateway } from "./gateway-coordinator";
@@ -523,7 +523,7 @@ export async function executePythonWithKernel(
523
523
  }
524
524
 
525
525
  export async function executePython(code: string, options?: PythonExecutorOptions): Promise<PythonResult> {
526
- const cwd = options?.cwd ?? process.cwd();
526
+ const cwd = options?.cwd ?? getProjectDir();
527
527
  await ensureKernelAvailable(cwd);
528
528
 
529
529
  const kernelMode = options?.kernelMode ?? "session";
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
- import { getAgentModulesDir, getProjectModulesDir } from "@oh-my-pi/pi-utils/dirs";
3
+ import { getAgentModulesDir, getProjectDir, getProjectModulesDir } from "@oh-my-pi/pi-utils/dirs";
4
4
 
5
5
  export type PythonModuleSource = "user" | "project";
6
6
 
@@ -24,7 +24,7 @@ export interface PythonModuleExecutor {
24
24
  }
25
25
 
26
26
  export interface DiscoverPythonModulesOptions {
27
- /** Working directory for project-level modules. Default: process.cwd() */
27
+ /** Working directory for project-level modules. Default: getProjectDir() */
28
28
  cwd?: string;
29
29
  /** Agent directory for user-level modules. Default: from getAgentDir() */
30
30
  agentDir?: string;
@@ -65,7 +65,7 @@ async function readModuleContent(candidate: ModuleCandidate): Promise<PythonModu
65
65
  * Discover Python prelude extension modules from user and project directories.
66
66
  */
67
67
  export async function discoverPythonModules(options: DiscoverPythonModulesOptions = {}): Promise<PythonModuleEntry[]> {
68
- const cwd = options.cwd ?? process.cwd();
68
+ const cwd = options.cwd ?? getProjectDir();
69
69
 
70
70
  const userDir = getAgentModulesDir(options.agentDir);
71
71
  const projectDir = getProjectModulesDir(cwd);
package/src/main.ts CHANGED
@@ -4,13 +4,15 @@
4
4
  * This file handles CLI argument parsing and translates them into
5
5
  * createAgentSession() options. The SDK does the heavy lifting.
6
6
  */
7
+
8
+ import { realpathSync } from "node:fs";
7
9
  import * as fs from "node:fs/promises";
8
10
  import * as os from "node:os";
9
11
  import * as path from "node:path";
10
12
  import { createInterface } from "node:readline/promises";
11
13
  import { type ImageContent, supportsXhigh } from "@oh-my-pi/pi-ai";
12
14
  import { $env, postmortem } from "@oh-my-pi/pi-utils";
13
- import { VERSION } from "@oh-my-pi/pi-utils/dirs";
15
+ import { getProjectDir, setProjectDir, VERSION } from "@oh-my-pi/pi-utils/dirs";
14
16
  import chalk from "chalk";
15
17
  import type { Args } from "./cli/args";
16
18
  import { processFileArguments } from "./cli/file-processor";
@@ -278,11 +280,11 @@ async function maybeAutoChdir(parsed: Args): Promise<void> {
278
280
  }
279
281
 
280
282
  const normalizePath = (value: string) => {
281
- const resolved = path.resolve(value);
283
+ const resolved = realpathSync(path.resolve(value));
282
284
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
283
285
  };
284
286
 
285
- const cwd = normalizePath(process.cwd());
287
+ const cwd = normalizePath(getProjectDir());
286
288
  const normalizedHome = normalizePath(home);
287
289
  if (cwd !== normalizedHome) {
288
290
  return;
@@ -303,7 +305,7 @@ async function maybeAutoChdir(parsed: Args): Promise<void> {
303
305
  if (!(await isDirectory(candidate))) {
304
306
  continue;
305
307
  }
306
- process.chdir(candidate);
308
+ setProjectDir(candidate);
307
309
  return;
308
310
  } catch {
309
311
  // Try next candidate.
@@ -313,7 +315,7 @@ async function maybeAutoChdir(parsed: Args): Promise<void> {
313
315
  try {
314
316
  const fallback = os.tmpdir();
315
317
  if (fallback && normalizePath(fallback) !== cwd && (await isDirectory(fallback))) {
316
- process.chdir(fallback);
318
+ setProjectDir(fallback);
317
319
  }
318
320
  } catch {
319
321
  // Ignore fallback errors.
@@ -355,7 +357,7 @@ async function buildSessionOptions(
355
357
  modelRegistry: ModelRegistry,
356
358
  ): Promise<CreateAgentSessionOptions> {
357
359
  const options: CreateAgentSessionOptions = {
358
- cwd: parsed.cwd ?? process.cwd(),
360
+ cwd: parsed.cwd ?? getProjectDir(),
359
361
  };
360
362
 
361
363
  // Auto-discover SYSTEM.md if no CLI system prompt provided
@@ -524,7 +526,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
524
526
  process.exit(1);
525
527
  }
526
528
 
527
- const cwd = process.cwd();
529
+ const cwd = getProjectDir();
528
530
  await Settings.init({ cwd });
529
531
  debugStartup("main:Settings.init");
530
532
  time("Settings.init");
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { readJsonl } from "@oh-my-pi/pi-utils";
9
+ import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
9
10
  import { type Subprocess, spawn } from "bun";
10
11
  import type { JsonRpcResponse, MCPRequestOptions, MCPStdioServerConfig, MCPTransport } from "../../mcp/types";
11
12
 
@@ -54,7 +55,7 @@ export class StdioTransport implements MCPTransport {
54
55
 
55
56
  this.#process = spawn({
56
57
  cmd: [this.config.command, ...args],
57
- cwd: this.config.cwd ?? process.cwd(),
58
+ cwd: this.config.cwd ?? getProjectDir(),
58
59
  env,
59
60
  stdin: "pipe",
60
61
  stdout: "pipe",
@@ -3,6 +3,7 @@ import * as path from "node:path";
3
3
  import type { AssistantMessage } from "@oh-my-pi/pi-ai";
4
4
  import { type Component, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
5
5
  import { isEnoent } from "@oh-my-pi/pi-utils";
6
+ import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
6
7
  import { theme } from "../../modes/theme/theme";
7
8
  import type { AgentSession } from "../../session/agent-session";
8
9
  import { shortenPath } from "../../tools/render-utils";
@@ -21,7 +22,7 @@ function sanitizeStatusText(text: string): string {
21
22
 
22
23
  /** Find the git root by walking up from cwd. Returns path and content of .git/HEAD if found. */
23
24
  async function findGitHeadPath(): Promise<{ path: string; content: string } | null> {
24
- let dir = process.cwd();
25
+ let dir = getProjectDir();
25
26
  while (true) {
26
27
  const gitHeadPath = path.join(dir, ".git", "HEAD");
27
28
  try {
@@ -201,7 +202,7 @@ export class FooterComponent implements Component {
201
202
  };
202
203
 
203
204
  // Replace home directory with ~
204
- let pwd = shortenPath(process.cwd());
205
+ let pwd = shortenPath(getProjectDir());
205
206
 
206
207
  // Add git branch if available
207
208
  const branch = this.#getCurrentBranch();