@oh-my-pi/pi-coding-agent 9.7.0 → 9.8.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [9.8.0] - 2026-02-01
6
+ ### Breaking Changes
7
+
8
+ - Removed persistent shell session support; bash execution now uses native bindings via brush-core for improved reliability
9
+
10
+ ### Added
11
+
12
+ - Added `sessionKey` option to bash executor to isolate shell sessions per agent instance
13
+ - Added shell snapshot support for bash execution to preserve shell state across commands
14
+ - Added `onChunk` callback support for streaming command output in real-time
15
+
16
+ ### Changed
17
+
18
+ - Refactored bash executor to queue output chunks asynchronously for improved reliability
19
+ - Updated bash executor to pass environment variables separately as `sessionEnv` to native bindings
20
+ - Migrated system information collection to use native bindings from brush-core instead of shell command execution
21
+ - Updated CPU information to report core count alongside model name
22
+ - Simplified OS version reporting to use Node.js built-in APIs
23
+ - Migrated bash command execution from ptree-based persistent sessions to native shell bindings with streaming support
24
+ - Simplified bash executor to use brush-core native API instead of managing long-lived shell processes
25
+ - Routed clipboard copy and image paste through native arboard bindings instead of shell commands
26
+ - Embedded native addon payload for compiled binaries and extract to `~/.omp/natives/<version>` on first run
27
+
28
+ ### Removed
29
+
30
+ - Removed shell configuration from environment information display
31
+ - Removed `shell-session.ts` module providing persistent shell session management
32
+ - Removed shell session test suite for persistent execution patterns
33
+
5
34
  ## [9.6.2] - 2026-02-01
6
35
  ### Changed
7
36
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "9.7.0",
3
+ "version": "9.8.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -74,17 +74,17 @@
74
74
  "scripts": {
75
75
  "check": "tsgo -p tsconfig.json",
76
76
  "format-prompts": "bun scripts/format-prompts.ts",
77
- "build:binary": "cd ../.. && bun build --compile --define OMP_COMPILED=true --root . ./packages/coding-agent/src/cli.ts --outfile packages/coding-agent/dist/omp",
77
+ "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",
78
78
  "generate-template": "bun scripts/generate-template.ts",
79
79
  "test": "bun test"
80
80
  },
81
81
  "dependencies": {
82
- "@oh-my-pi/omp-stats": "9.7.0",
83
- "@oh-my-pi/pi-agent-core": "9.7.0",
84
- "@oh-my-pi/pi-ai": "9.7.0",
85
- "@oh-my-pi/pi-natives": "9.7.0",
86
- "@oh-my-pi/pi-tui": "9.7.0",
87
- "@oh-my-pi/pi-utils": "9.7.0",
82
+ "@oh-my-pi/omp-stats": "9.8.0",
83
+ "@oh-my-pi/pi-agent-core": "9.8.0",
84
+ "@oh-my-pi/pi-ai": "9.8.0",
85
+ "@oh-my-pi/pi-natives": "9.8.0",
86
+ "@oh-my-pi/pi-tui": "9.8.0",
87
+ "@oh-my-pi/pi-utils": "9.8.0",
88
88
  "@openai/agents": "^0.4.4",
89
89
  "@sinclair/typebox": "^0.34.48",
90
90
  "ajv": "^8.17.1",
@@ -1,19 +1,20 @@
1
1
  /**
2
2
  * Bash command execution with streaming support and cancellation.
3
3
  *
4
- * Provides unified bash execution for AgentSession.executeBash() and direct calls.
4
+ * Uses brush-core via native bindings for shell execution.
5
5
  */
6
- import { Exception, ptree } from "@oh-my-pi/pi-utils";
6
+ import { Shell } from "@oh-my-pi/pi-natives";
7
7
  import { Settings } from "../config/settings";
8
8
  import { OutputSink } from "../session/streaming-output";
9
- import { getOrCreateSnapshot, getSnapshotSourceCommand } from "../utils/shell-snapshot";
10
- import { executeShellCommand } from "./shell-session";
9
+ import { getOrCreateSnapshot } from "../utils/shell-snapshot";
11
10
 
12
11
  export interface BashExecutorOptions {
13
12
  cwd?: string;
14
13
  timeout?: number;
15
14
  onChunk?: (chunk: string) => void;
16
15
  signal?: AbortSignal;
16
+ /** Session key suffix to isolate shell sessions per agent */
17
+ sessionKey?: string;
17
18
  /** Additional environment variables to inject */
18
19
  env?: Record<string, string>;
19
20
  /** Artifact path/id for full output storage */
@@ -33,116 +34,114 @@ export interface BashResult {
33
34
  artifactId?: string;
34
35
  }
35
36
 
37
+ const shellSessions = new Map<string, Shell>();
38
+
36
39
  export async function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {
37
40
  const settings = await Settings.init();
38
- const { shell, args, env, prefix } = settings.getShellConfig();
39
- const snapshotPath = await getOrCreateSnapshot(shell, env);
40
-
41
- if (shouldUsePersistentShell(settings.get("bash.persistentShell"))) {
42
- return await executeShellCommand({ shell, env, prefix, snapshotPath }, command, {
43
- cwd: options?.cwd,
44
- timeout: options?.timeout,
45
- signal: options?.signal,
46
- onChunk: options?.onChunk,
47
- env: options?.env,
48
- artifactPath: options?.artifactPath,
49
- artifactId: options?.artifactId,
50
- });
51
- }
52
-
53
- return await executeBashOnce(command, options, { shell, args, env, prefix, snapshotPath });
54
- }
55
-
56
- /**
57
- * Determine whether to use persistent shell sessions.
58
- * Priority: OMP_SHELL_PERSIST env var > settings > default (false)
59
- */
60
- function shouldUsePersistentShell(settingValue: boolean): boolean {
61
- // Env var takes precedence (for debugging/override)
62
- const flag = parseEnvFlag(process.env.OMP_SHELL_PERSIST);
63
- if (flag !== undefined) return flag;
64
- // Windows never uses persistent shell (too unreliable)
65
- if (process.platform === "win32") return false;
66
- // Use setting value (defaults to false)
67
- return settingValue;
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
- }
41
+ const { shell, env: shellEnv, prefix } = settings.getShellConfig();
42
+ const snapshotPath = shell.includes("bash") ? await getOrCreateSnapshot(shell, shellEnv) : null;
77
43
 
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;
90
-
91
- // Merge additional env vars if provided
92
- const finalEnv = options?.env ? { ...env, ...options.env } : env;
93
- const snapshotPrefix = getSnapshotSourceCommand(snapshotPath);
44
+ // Apply command prefix if configured
94
45
  const prefixedCommand = prefix ? `${prefix} ${command}` : command;
95
- const finalCommand = `${snapshotPrefix}${prefixedCommand}`;
46
+ const finalCommand = prefixedCommand;
96
47
 
48
+ // Create output sink for truncation and artifact handling
97
49
  const sink = new OutputSink({
98
50
  onChunk: options?.onChunk,
99
51
  artifactPath: options?.artifactPath,
100
52
  artifactId: options?.artifactId,
101
53
  });
102
54
 
103
- using child = ptree.spawn([shell, ...args, finalCommand], {
104
- cwd: options?.cwd,
105
- env: finalEnv,
106
- signal: options?.signal,
107
- timeout: options?.timeout,
108
- });
55
+ let pendingChunks = Promise.resolve();
56
+ const enqueueChunk = (chunk: string) => {
57
+ pendingChunks = pendingChunks.then(() => sink.push(chunk)).catch(() => {});
58
+ };
109
59
 
110
- // Pump streams - errors during abort/timeout are expected
111
- // Use preventClose to avoid closing the shared sink when either stream finishes
112
- await Promise.allSettled([child.stdout.pipeTo(sink.createInput()), child.stderr.pipeTo(sink.createInput())]).catch(
113
- () => {},
114
- );
60
+ if (options?.signal?.aborted) {
61
+ return {
62
+ exitCode: undefined,
63
+ cancelled: true,
64
+ ...(await sink.dump("Command cancelled")),
65
+ };
66
+ }
67
+
68
+ let abortListener: (() => void) | undefined;
115
69
 
116
- // Wait for process exit
117
70
  try {
71
+ const sessionKey = buildSessionKey(shell, prefix, snapshotPath, shellEnv, options?.sessionKey);
72
+ let shellSession = shellSessions.get(sessionKey);
73
+ if (!shellSession) {
74
+ shellSession = new Shell({ sessionEnv: shellEnv, snapshotPath: snapshotPath ?? undefined });
75
+ shellSessions.set(sessionKey, shellSession);
76
+ }
77
+
78
+ if (options?.signal) {
79
+ abortListener = () => {
80
+ shellSession?.abort();
81
+ };
82
+ options.signal.addEventListener("abort", abortListener, { once: true });
83
+ }
84
+
85
+ const result = await shellSession.run(
86
+ {
87
+ command: finalCommand,
88
+ cwd: options?.cwd,
89
+ env: options?.env,
90
+ timeoutMs: options?.timeout,
91
+ },
92
+ (err, chunk) => {
93
+ if (!err) {
94
+ enqueueChunk(chunk);
95
+ }
96
+ },
97
+ );
98
+
99
+ await pendingChunks;
100
+
101
+ // Handle timeout
102
+ if (result.timedOut) {
103
+ const annotation = options?.timeout
104
+ ? `Command timed out after ${Math.round(options.timeout / 1000)} seconds`
105
+ : "Command timed out";
106
+ return {
107
+ exitCode: undefined,
108
+ cancelled: true,
109
+ ...(await sink.dump(annotation)),
110
+ };
111
+ }
112
+
113
+ // Handle cancellation
114
+ if (result.cancelled) {
115
+ return {
116
+ exitCode: undefined,
117
+ cancelled: true,
118
+ ...(await sink.dump("Command cancelled")),
119
+ };
120
+ }
121
+
122
+ // Normal completion
118
123
  return {
119
- exitCode: await child.exited,
124
+ exitCode: result.exitCode,
120
125
  cancelled: false,
121
126
  ...(await sink.dump()),
122
127
  };
123
- } catch (err: unknown) {
124
- // Exception covers NonZeroExitError, AbortError, TimeoutError
125
- if (err instanceof Exception) {
126
- if (err.aborted) {
127
- const isTimeout = err instanceof ptree.TimeoutError || err.message.toLowerCase().includes("timed out");
128
- const annotation = isTimeout
129
- ? `Command timed out after ${Math.round((options?.timeout ?? 0) / 1000)} seconds`
130
- : undefined;
131
- return {
132
- exitCode: undefined,
133
- cancelled: true,
134
- ...(await sink.dump(annotation)),
135
- };
136
- }
137
-
138
- // NonZeroExitError
139
- return {
140
- exitCode: err.exitCode,
141
- cancelled: false,
142
- ...(await sink.dump()),
143
- };
128
+ } finally {
129
+ await pendingChunks;
130
+ if (options?.signal && abortListener) {
131
+ options.signal.removeEventListener("abort", abortListener);
144
132
  }
145
-
146
- throw err;
147
133
  }
148
134
  }
135
+
136
+ function buildSessionKey(
137
+ shell: string,
138
+ prefix: string | undefined,
139
+ snapshotPath: string | null,
140
+ env: Record<string, string>,
141
+ agentSessionKey?: string,
142
+ ): string {
143
+ const entries = Object.entries(env);
144
+ entries.sort(([a], [b]) => a.localeCompare(b));
145
+ const envSerialized = entries.map(([key, value]) => `${key}=${value}`).join("\n");
146
+ return [agentSessionKey ?? "", shell, prefix ?? "", snapshotPath ?? "", envSerialized].join("\n");
147
+ }
@@ -424,7 +424,8 @@ export async function executePython(code: string, options?: PythonExecutorOption
424
424
  await ensureKernelAvailable(cwd);
425
425
 
426
426
  const kernelMode = options?.kernelMode ?? "session";
427
- const useSharedGateway = options?.useSharedGateway;
427
+ const isTestEnv = process.env.BUN_ENV === "test" || process.env.NODE_ENV === "test";
428
+ const useSharedGateway = isTestEnv ? false : options?.useSharedGateway;
428
429
  const sessionFile = options?.sessionFile;
429
430
  const artifactsDir = options?.artifactsDir;
430
431
 
@@ -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 type { UsageLimit, UsageReport } from "@oh-my-pi/pi-ai";
5
+ import { copyToClipboard } from "@oh-my-pi/pi-natives";
5
6
  import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui";
6
7
  import { $ } from "bun";
7
8
  import { nanoid } from "nanoid";
@@ -18,7 +19,6 @@ import type { InteractiveModeContext } from "../../modes/types";
18
19
  import { createCompactionSummaryMessage } from "../../session/messages";
19
20
  import { outputMeta } from "../../tools/output-meta";
20
21
  import { getChangelogPath, parseChangelog } from "../../utils/changelog";
21
- import { copyToClipboard } from "../../utils/clipboard";
22
22
 
23
23
  export class CommandController {
24
24
  constructor(private readonly ctx: InteractiveModeContext) {}
@@ -3,13 +3,13 @@ import * as fs from "node:fs/promises";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
6
+ import { readImageFromClipboard } from "@oh-my-pi/pi-natives";
6
7
  import { nanoid } from "nanoid";
7
8
  import { settings } from "../../config/settings";
8
9
  import { theme } from "../../modes/theme/theme";
9
10
  import type { InteractiveModeContext } from "../../modes/types";
10
11
  import type { AgentSessionEvent } from "../../session/agent-session";
11
12
  import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
12
- import { readImageFromClipboard } from "../../utils/clipboard";
13
13
  import { resizeImage } from "../../utils/image-resize";
14
14
  import { generateSessionTitle, setTerminalTitle } from "../../utils/title-generator";
15
15
 
@@ -563,17 +563,18 @@ export class InputController {
563
563
  try {
564
564
  const image = await readImageFromClipboard();
565
565
  if (image) {
566
- let imageData = image;
566
+ const base64Data = Buffer.from(image.data).toString("base64");
567
+ let imageData = { data: base64Data, mimeType: image.mimeType };
567
568
  if (settings.get("images.autoResize")) {
568
569
  try {
569
570
  const resized = await resizeImage({
570
571
  type: "image",
571
- data: image.data,
572
+ data: base64Data,
572
573
  mimeType: image.mimeType,
573
574
  });
574
575
  imageData = { data: resized.data, mimeType: resized.mimeType };
575
576
  } catch {
576
- imageData = image;
577
+ imageData = { data: base64Data, mimeType: image.mimeType };
577
578
  }
578
579
  }
579
580
 
@@ -3080,6 +3080,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
3080
3080
  const result = await executeBashCommand(command, {
3081
3081
  onChunk,
3082
3082
  signal: this._bashAbortController.signal,
3083
+ sessionKey: this.sessionId,
3083
3084
  });
3084
3085
 
3085
3086
  this.recordBashResult(command, result, options);