@oh-my-pi/pi-coding-agent 9.2.1 → 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,87 @@
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
+
32
+ ## [9.2.2] - 2026-01-31
33
+
34
+ ### Added
35
+ - Added grep CLI subcommand (`omp grep`) for testing pattern matching
36
+ - Added fuzzy matching for model resolution with scoring and ranking fallback
37
+ - Added 'Open: artifact folder' menu option to debug selector for quick access to session artifacts
38
+ - Added Kimi API format setting for selecting between OpenAI and Anthropic formats
39
+ - Added Codex and Gemini web search providers with OAuth and grounding support
40
+ - Added /debug command with interactive menu for profiling, heap snapshots, session dumps, and diagnostics
41
+ - Added configurable ask timeout and notification settings
42
+ - Added gitignore-aware project tree scanning with ripgrep integration
43
+ - Added project tree visualization to system prompts with configurable depth and entry limits
44
+ - Added reset() method to CountdownTimer with integration into HookSelectorComponent
45
+ - Added custom message support to AgentSession via promptCustomMessage() method
46
+ - Added skill message component for rendering /skill command messages as compact entries
47
+ - Added model preference matching system for intelligent model selection based on usage history
48
+ - Added designer agent with UI/UX review and accessibility audit capabilities
49
+ - Added model-specific edit variant configuration for patch/replace modes
50
+ - Added automatic browser opening when stats dashboard starts
51
+ - Added model statistics table and TTFT/throughput metrics to stats dashboard
52
+ - Added artifact allocation for truncated fetch responses to preserve full content
53
+ - Added 30-second timeout to ask tool with auto-selection of recommended option
54
+ - Added recommended parameter (0-indexed) to ask tool for specifying default option
55
+ - Added JTD to TypeScript converter for rendering schemas in system prompts
56
+ - Added tools list to system prompt for better agent awareness
57
+ - Added synthetic message flag for system-injected prompts
58
+ - Added session compaction enhancements with auto-continue, tool pruning, and remote endpoint support
59
+ - Added detection and rendering of missing complete tool warning in subagent output
60
+ - Added outline UI components for bordered list containers
61
+ - Added macOS NFD normalization and curly quote variant resolution for file paths
62
+ - Enhanced session compaction with dynamic token ratio adjustment and improved summary preservation
63
+
64
+ ### Changed
65
+ - Simplified find tool API by consolidating path and pattern parameters
66
+ - Replaced bulk file loading with streaming for read tool to reduce memory overhead
67
+ - Migrated grep and find tools to WASM-based implementation
68
+ - Replaced ripgrep-based file listing with glob-based file discovery for project scans
69
+ - Updated minimum Bun runtime requirement to >=1.3.7
70
+ - Renamed task parameter from output to schema
71
+ - Renamed complete tool to submit_result for clarity and consistency
72
+ - Improved output preview logic: shows full output for ≤30 lines, truncates to 10 lines for larger output
73
+
74
+ ### Fixed
75
+ - Enhanced error reporting with debug stack trace when DEBUG env is set
76
+ - Improved OAuth token refresh error handling to distinguish transient vs definitive failures
77
+ - Added windowsHide option to child process spawn calls to prevent console windows on Windows
78
+ - External edits to config.yml are now preserved when omp reloads or saves settings
79
+ - Exposed LSP server startup errors in session display and logs
80
+ - Improved error handling and security in agent storage initialization with restrictive file permissions
81
+ - Fixed LSP server display showing unknown when server warmup fails
82
+ - Preserved null timeout when user disables ask timeout setting
83
+ - Removed incorrect timeout unit conversion logic in cursor, fetch, gemini-image, and ssh tools
84
+ - Blocked /fork command while streaming to prevent split session logs
85
+
5
86
  ## [9.0.0] - 2026-01-29
6
87
 
7
88
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "9.2.1",
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.1",
83
- "@oh-my-pi/pi-agent-core": "9.2.1",
84
- "@oh-my-pi/pi-ai": "9.2.1",
85
- "@oh-my-pi/pi-natives": "9.2.1",
86
- "@oh-my-pi/pi-tui": "9.2.1",
87
- "@oh-my-pi/pi-utils": "9.2.1",
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;
@@ -113,6 +113,10 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
113
113
  messageCount += 1;
114
114
  isThinking = false;
115
115
  clearThinkingLine();
116
+ const assistantMessage = event.message as { stopReason?: string; errorMessage?: string };
117
+ if (assistantMessage.stopReason === "error" && assistantMessage.errorMessage) {
118
+ writeStdout(`● Error: ${assistantMessage.errorMessage}`);
119
+ }
116
120
  const messageText = extractMessageText(event.message?.content ?? []);
117
121
  if (messageText) {
118
122
  writeAssistantMessage(messageText);
@@ -134,7 +134,11 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
134
134
  existingChangelogEntries,
135
135
  });
136
136
  } catch (error) {
137
- writeStderr(`Agent error: ${error instanceof Error ? error.message : String(error)}`);
137
+ const errorMessage = error instanceof Error ? error.message : String(error);
138
+ writeStderr(`Agent error: ${errorMessage}`);
139
+ if (error instanceof Error && error.stack && process.env.DEBUG) {
140
+ writeStderr(error.stack);
141
+ }
138
142
  writeStdout("● Using fallback commit generation...");
139
143
  commitState = { proposal: generateFallbackProposal(numstat) };
140
144
  usedFallback = true;
@@ -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