@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 +29 -0
- package/package.json +8 -8
- package/src/exec/bash-executor.ts +95 -96
- package/src/ipy/executor.ts +2 -1
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +5 -4
- package/src/session/agent-session.ts +1 -0
- package/src/system-prompt.ts +24 -244
- package/src/utils/image-convert.ts +3 -3
- package/src/utils/image-resize.ts +10 -7
- package/src/exec/shell-session.ts +0 -604
- package/src/utils/clipboard.ts +0 -248
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.
|
|
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.
|
|
83
|
-
"@oh-my-pi/pi-agent-core": "9.
|
|
84
|
-
"@oh-my-pi/pi-ai": "9.
|
|
85
|
-
"@oh-my-pi/pi-natives": "9.
|
|
86
|
-
"@oh-my-pi/pi-tui": "9.
|
|
87
|
-
"@oh-my-pi/pi-utils": "9.
|
|
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
|
-
*
|
|
4
|
+
* Uses brush-core via native bindings for shell execution.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
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
|
|
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,
|
|
39
|
-
const snapshotPath = await getOrCreateSnapshot(shell,
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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:
|
|
124
|
+
exitCode: result.exitCode,
|
|
120
125
|
cancelled: false,
|
|
121
126
|
...(await sink.dump()),
|
|
122
127
|
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
-
|
|
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
|
+
}
|
package/src/ipy/executor.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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:
|
|
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);
|