@oh-my-pi/pi-coding-agent 9.2.2 → 9.2.3

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [9.2.3] - 2026-01-31
6
+ ### Added
7
+
8
+ - Persistent shell session support for bash tool with environment variable preservation across commands
9
+ - New `shellForceBasic` setting to force bash/sh even if user's default shell is different (default: true)
10
+ - New `OMP_SHELL_PERSIST` environment variable to control persistent shell behavior (set to 0 to disable)
11
+
12
+ ### Changed
13
+
14
+ - Bash tool now reuses a persistent shell session by default on Unix systems for improved performance and state preservation
15
+ - Replaced Bun file APIs with Node.js `fs` module for better cross-runtime compatibility
16
+ - LSP configuration loading is now synchronous instead of async
17
+ - Shell snapshot generation now sanitizes `BASH_ENV` and `ENV` variables to prevent shell exit issues
18
+ - Shell snapshot caching now per-shell-binary instead of global to avoid cross-shell contamination
19
+ - System prompt restructured with coordinator-specific guidance for parallel task delegation
20
+ - Bash tool now reuses a persistent shell session by default on Unix. Set `OMP_SHELL_PERSIST=0` to disable or fall back to per-command execution on Windows/unsupported shells.
21
+ - Added a shellForceBasic setting to force bash/sh and keep environment changes across bash commands (default: true).
22
+
23
+ ### Fixed
24
+
25
+ - Shell snapshots now filter unsafe bash options (onecmd, monitor, restricted) to prevent session exits
26
+ - Git branch detection in status line now works synchronously without race conditions
27
+ - Shell session initialization properly restores trap handlers and shell functions after command execution
28
+ - Sanitized `BASH_ENV`/`ENV` during persistent shell startup and snapshot creation to prevent basic shells from exiting immediately.
29
+ - Cached shell snapshots per shell binary to avoid sourcing zsh snapshots in bash sessions.
30
+ - Filtered unsafe bash options (onecmd/monitor/restricted) out of shell snapshots to prevent session exits.
31
+
5
32
  ## [9.2.2] - 2026-01-31
6
33
 
7
34
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "9.2.2",
3
+ "version": "9.2.3",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -79,12 +79,12 @@
79
79
  "test": "bun test"
80
80
  },
81
81
  "dependencies": {
82
- "@oh-my-pi/omp-stats": "9.2.2",
83
- "@oh-my-pi/pi-agent-core": "9.2.2",
84
- "@oh-my-pi/pi-ai": "9.2.2",
85
- "@oh-my-pi/pi-natives": "9.2.2",
86
- "@oh-my-pi/pi-tui": "9.2.2",
87
- "@oh-my-pi/pi-utils": "9.2.2",
82
+ "@oh-my-pi/omp-stats": "9.2.3",
83
+ "@oh-my-pi/pi-agent-core": "9.2.3",
84
+ "@oh-my-pi/pi-ai": "9.2.3",
85
+ "@oh-my-pi/pi-natives": "9.2.3",
86
+ "@oh-my-pi/pi-tui": "9.2.3",
87
+ "@oh-my-pi/pi-utils": "9.2.3",
88
88
  "@openai/agents": "^0.4.4",
89
89
  "@sinclair/typebox": "^0.34.48",
90
90
  "ajv": "^8.17.1",
@@ -195,15 +195,10 @@ async function updateViaBinary(release: ReleaseInfo): Promise<void> {
195
195
  console.log(chalk.green(`\n${theme.status.success} Updated to ${release.version}`));
196
196
  console.log(chalk.dim(`Restart ${APP_NAME} to use the new version`));
197
197
  } catch (err) {
198
- const [backupExists, execExists, tempExists] = await Promise.all([
199
- Bun.file(backupPath).exists(),
200
- Bun.file(execPath).exists(),
201
- Bun.file(tempPath).exists(),
202
- ]);
203
- if (backupExists && !execExists) {
198
+ if (fs.existsSync(backupPath) && !fs.existsSync(execPath)) {
204
199
  await fs.promises.rename(backupPath, execPath);
205
200
  }
206
- if (tempExists) {
201
+ if (fs.existsSync(tempPath)) {
207
202
  await fs.promises.unlink(tempPath);
208
203
  }
209
204
  throw err;
@@ -1,3 +1,4 @@
1
+ import * as fs from "node:fs";
1
2
  import * as path from "node:path";
2
3
  import type { ChangelogBoundary } from "../../commit/types";
3
4
 
@@ -25,7 +26,7 @@ async function findNearestChangelog(cwd: string, filePath: string): Promise<stri
25
26
  const root = path.resolve(cwd);
26
27
  while (true) {
27
28
  const candidate = path.resolve(current, CHANGELOG_NAME);
28
- if (await Bun.file(candidate).exists()) {
29
+ if (fs.existsSync(candidate)) {
29
30
  return candidate;
30
31
  }
31
32
  if (current === root) return null;
@@ -1,3 +1,4 @@
1
+ import * as fs from "node:fs";
1
2
  import * as path from "node:path";
2
3
  import type { Api, Model } from "@oh-my-pi/pi-ai";
3
4
  import { logger } from "@oh-my-pi/pi-utils";
@@ -108,7 +109,7 @@ export async function applyChangelogProposals({
108
109
  )
109
110
  continue;
110
111
  onProgress?.(`Applying entries for ${proposal.path}...`);
111
- const exists = await Bun.file(proposal.path).exists();
112
+ const exists = fs.existsSync(proposal.path);
112
113
  if (!exists) {
113
114
  logger.warn("commit changelog path missing", { path: proposal.path });
114
115
  continue;
@@ -1,3 +1,4 @@
1
+ import * as fs from "node:fs";
1
2
  import * as path from "node:path";
2
3
  import { logger } from "@oh-my-pi/pi-utils";
3
4
  import Handlebars from "handlebars";
@@ -435,7 +436,7 @@ async function loadTemplatesFromDir(
435
436
  }
436
437
  }
437
438
  } catch (error) {
438
- if (!(await Bun.file(dir).exists())) {
439
+ if (!fs.existsSync(dir)) {
439
440
  return [];
440
441
  }
441
442
  logger.warn("Failed to scan prompt templates directory", { dir, error: String(error) });
@@ -1,4 +1,4 @@
1
- import * as fs from "node:fs/promises";
1
+ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
4
4
  import { YAML } from "bun";
@@ -222,6 +222,7 @@ export interface Settings {
222
222
  retry?: RetrySettings;
223
223
  hideThinkingBlock?: boolean;
224
224
  shellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)
225
+ shellForceBasic?: boolean; // Force bash/sh even if user's default shell is different
225
226
  collapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full)
226
227
  startup?: StartupSettings;
227
228
  doubleEscapeAction?: "branch" | "tree"; // Action for double-escape with empty editor (default: "tree")
@@ -635,7 +636,7 @@ export class SettingsManager {
635
636
  migrated = true;
636
637
  // Backup settings.json
637
638
  try {
638
- await fs.rename(settingsJsonPath, `${settingsJsonPath}.bak`);
639
+ fs.renameSync(settingsJsonPath, `${settingsJsonPath}.bak`);
639
640
  } catch (error) {
640
641
  logger.warn("SettingsManager failed to backup settings.json", { error: String(error) });
641
642
  }
@@ -1061,12 +1062,22 @@ export class SettingsManager {
1061
1062
  return this.settings.shellPath;
1062
1063
  }
1063
1064
 
1065
+ getShellForceBasic(): boolean {
1066
+ return this.settings.shellForceBasic ?? true;
1067
+ }
1068
+
1064
1069
  async setShellPath(path: string | undefined): Promise<void> {
1065
1070
  this.globalSettings.shellPath = path;
1066
1071
  this.markModified("shellPath");
1067
1072
  await this.save();
1068
1073
  }
1069
1074
 
1075
+ async setShellForceBasic(force: boolean): Promise<void> {
1076
+ this.globalSettings.shellForceBasic = force;
1077
+ this.markModified("shellForceBasic");
1078
+ await this.save();
1079
+ }
1080
+
1070
1081
  getCollapseChangelog(): boolean {
1071
1082
  return this.settings.collapseChangelog ?? false;
1072
1083
  }
@@ -1963,7 +1974,13 @@ export class SettingsManager {
1963
1974
  * Gets the shell configuration
1964
1975
  * @returns The shell configuration
1965
1976
  */
1966
- async getShellConfig() {
1977
+ getShellConfig() {
1978
+ if (this.getShellForceBasic()) {
1979
+ const basicShell = resolveBasicShell();
1980
+ if (basicShell) {
1981
+ return procmgr.getShellConfig(basicShell);
1982
+ }
1983
+ }
1967
1984
  const shell = this.getShellPath();
1968
1985
  return procmgr.getShellConfig(shell);
1969
1986
  }
@@ -1977,3 +1994,22 @@ export class SettingsManager {
1977
1994
  return settings.getShellConfig();
1978
1995
  }
1979
1996
  }
1997
+
1998
+ function resolveBasicShell(): string | undefined {
1999
+ const searchPaths = ["/bin", "/usr/bin", "/usr/local/bin", "/opt/homebrew/bin"];
2000
+ const candidates = ["bash", "sh"];
2001
+
2002
+ for (const name of candidates) {
2003
+ for (const dir of searchPaths) {
2004
+ const fullPath = path.join(dir, name);
2005
+ if (fs.existsSync(fullPath)) return fullPath;
2006
+ }
2007
+ }
2008
+
2009
+ for (const name of ["bash", "bash.exe", "sh", "sh.exe"]) {
2010
+ const resolved = Bun.which(name);
2011
+ if (resolved) return resolved;
2012
+ }
2013
+
2014
+ return undefined;
2015
+ }
@@ -7,6 +7,7 @@ import { Exception, ptree } from "@oh-my-pi/pi-utils";
7
7
  import { SettingsManager } from "../config/settings-manager";
8
8
  import { OutputSink } from "../session/streaming-output";
9
9
  import { getOrCreateSnapshot, getSnapshotSourceCommand } from "../utils/shell-snapshot";
10
+ import { executeShellCommand } from "./shell-session";
10
11
 
11
12
  export interface BashExecutorOptions {
12
13
  cwd?: string;
@@ -34,13 +35,62 @@ export interface BashResult {
34
35
 
35
36
  export async function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {
36
37
  const { shell, args, env, prefix } = await SettingsManager.getGlobalShellConfig();
38
+ const snapshotPath = await getOrCreateSnapshot(shell, env);
39
+
40
+ if (shouldUsePersistentShell(shell)) {
41
+ return await executeShellCommand({ shell, env, prefix, snapshotPath }, command, {
42
+ cwd: options?.cwd,
43
+ timeout: options?.timeout,
44
+ signal: options?.signal,
45
+ onChunk: options?.onChunk,
46
+ env: options?.env,
47
+ artifactPath: options?.artifactPath,
48
+ artifactId: options?.artifactId,
49
+ });
50
+ }
51
+
52
+ return await executeBashOnce(command, options, { shell, args, env, prefix, snapshotPath });
53
+ }
54
+
55
+ function shouldUsePersistentShell(shell: string): boolean {
56
+ const flag = parseEnvFlag(process.env.OMP_SHELL_PERSIST);
57
+ if (flag !== undefined) return flag;
58
+ if (process.platform === "win32") return false;
59
+ const normalized = shell.toLowerCase();
60
+ return (
61
+ normalized.includes("bash") ||
62
+ normalized.includes("zsh") ||
63
+ normalized.includes("fish") ||
64
+ normalized.endsWith("/sh") ||
65
+ normalized.endsWith("\\\\sh") ||
66
+ normalized.endsWith("sh")
67
+ );
68
+ }
69
+
70
+ function parseEnvFlag(value: string | undefined): boolean | undefined {
71
+ if (!value) return undefined;
72
+ const normalized = value.toLowerCase();
73
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
74
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
75
+ return undefined;
76
+ }
77
+
78
+ async function executeBashOnce(
79
+ command: string,
80
+ options: BashExecutorOptions | undefined,
81
+ config: {
82
+ shell: string;
83
+ args: string[];
84
+ env: Record<string, string | undefined>;
85
+ prefix?: string;
86
+ snapshotPath: string | null;
87
+ },
88
+ ): Promise<BashResult> {
89
+ const { shell, args, env, prefix, snapshotPath } = config;
37
90
 
38
91
  // Merge additional env vars if provided
39
92
  const finalEnv = options?.env ? { ...env, ...options.env } : env;
40
-
41
- const snapshotPath = await getOrCreateSnapshot(shell, env);
42
93
  const snapshotPrefix = getSnapshotSourceCommand(snapshotPath);
43
-
44
94
  const prefixedCommand = prefix ? `${prefix} ${command}` : command;
45
95
  const finalCommand = `${snapshotPrefix}${prefixedCommand}`;
46
96