@oh-my-pi/pi-coding-agent 10.0.0 → 10.2.1
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 +12 -0
- package/package.json +7 -7
- package/src/debug/index.ts +40 -0
- package/src/debug/report-bundle.ts +18 -0
- package/src/exec/bash-executor.ts +1 -12
- package/src/system-prompt.ts +3 -3
- package/src/tools/bash-normalize.ts +2 -21
- package/src/tools/find.ts +7 -7
- package/src/tools/read.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [10.2.1] - 2026-02-02
|
|
6
|
+
### Breaking Changes
|
|
7
|
+
|
|
8
|
+
- Removed `strippedRedirect` field from `NormalizedCommand` interface returned by `normalizeBashCommand()`
|
|
9
|
+
- Removed automatic stripping of `2>&1` stderr redirections from bash command normalization
|
|
10
|
+
|
|
11
|
+
## [10.1.0] - 2026-02-01
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added work scheduling profiler to debug menu for analyzing CPU scheduling patterns over the last 30 seconds
|
|
15
|
+
- Added support for work profile data in report bundles including folded stacks, summary, and flamegraph visualization
|
|
16
|
+
|
|
5
17
|
## [10.0.0] - 2026-02-01
|
|
6
18
|
### Added
|
|
7
19
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.2.1",
|
|
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": "10.
|
|
83
|
-
"@oh-my-pi/pi-agent-core": "10.
|
|
84
|
-
"@oh-my-pi/pi-ai": "10.
|
|
85
|
-
"@oh-my-pi/pi-natives": "10.
|
|
86
|
-
"@oh-my-pi/pi-tui": "10.
|
|
87
|
-
"@oh-my-pi/pi-utils": "10.
|
|
82
|
+
"@oh-my-pi/omp-stats": "10.2.1",
|
|
83
|
+
"@oh-my-pi/pi-agent-core": "10.2.1",
|
|
84
|
+
"@oh-my-pi/pi-ai": "10.2.1",
|
|
85
|
+
"@oh-my-pi/pi-natives": "10.2.1",
|
|
86
|
+
"@oh-my-pi/pi-tui": "10.2.1",
|
|
87
|
+
"@oh-my-pi/pi-utils": "10.2.1",
|
|
88
88
|
"@openai/agents": "^0.4.4",
|
|
89
89
|
"@sinclair/typebox": "^0.34.48",
|
|
90
90
|
"ajv": "^8.17.1",
|
package/src/debug/index.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Provides tools for debugging, bug report generation, and system diagnostics.
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from "node:fs/promises";
|
|
7
|
+
import { getWorkProfile } from "@oh-my-pi/pi-natives/work";
|
|
7
8
|
import { Container, Loader, type SelectItem, SelectList, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
8
9
|
import { getSessionsDir } from "../config";
|
|
9
10
|
import { DynamicBorder } from "../modes/components/dynamic-border";
|
|
@@ -17,6 +18,7 @@ import { collectSystemInfo, formatSystemInfo } from "./system-info";
|
|
|
17
18
|
const DEBUG_MENU_ITEMS: SelectItem[] = [
|
|
18
19
|
{ value: "open-artifacts", label: "Open: artifact folder", description: "Open session artifacts in file manager" },
|
|
19
20
|
{ value: "performance", label: "Report: performance issue", description: "Profile CPU, reproduce, then bundle" },
|
|
21
|
+
{ value: "work", label: "Profile: work scheduling", description: "Open flamegraph of last 30s" },
|
|
20
22
|
{ value: "dump", label: "Report: dump session", description: "Create report bundle immediately" },
|
|
21
23
|
{ value: "memory", label: "Report: memory issue", description: "Heap snapshot + bundle" },
|
|
22
24
|
{ value: "logs", label: "View: recent logs", description: "Show last 50 log entries" },
|
|
@@ -69,6 +71,9 @@ export class DebugSelectorComponent extends Container {
|
|
|
69
71
|
case "performance":
|
|
70
72
|
await this.handlePerformanceReport();
|
|
71
73
|
break;
|
|
74
|
+
case "work":
|
|
75
|
+
await this.handleWorkReport();
|
|
76
|
+
break;
|
|
72
77
|
case "dump":
|
|
73
78
|
await this.handleDumpReport();
|
|
74
79
|
break;
|
|
@@ -138,10 +143,12 @@ export class DebugSelectorComponent extends Container {
|
|
|
138
143
|
|
|
139
144
|
try {
|
|
140
145
|
const cpuProfile = await session.stop();
|
|
146
|
+
const workProfile = getWorkProfile(30);
|
|
141
147
|
const result = await createReportBundle({
|
|
142
148
|
sessionFile: this.ctx.sessionManager.getSessionFile(),
|
|
143
149
|
settings: this.getResolvedSettings(),
|
|
144
150
|
cpuProfile,
|
|
151
|
+
workProfile,
|
|
145
152
|
});
|
|
146
153
|
|
|
147
154
|
loader.stop();
|
|
@@ -162,6 +169,39 @@ export class DebugSelectorComponent extends Container {
|
|
|
162
169
|
this.ctx.ui.requestRender();
|
|
163
170
|
}
|
|
164
171
|
|
|
172
|
+
private async handleWorkReport(): Promise<void> {
|
|
173
|
+
try {
|
|
174
|
+
const workProfile = getWorkProfile(30);
|
|
175
|
+
|
|
176
|
+
if (!workProfile.svg) {
|
|
177
|
+
this.ctx.showWarning(`No work profile data (${workProfile.sampleCount} samples)`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Write SVG to temp file and open in browser
|
|
182
|
+
const tmpPath = `/tmp/work-profile-${Date.now()}.svg`;
|
|
183
|
+
await Bun.write(tmpPath, workProfile.svg);
|
|
184
|
+
|
|
185
|
+
const openCmd =
|
|
186
|
+
process.platform === "darwin"
|
|
187
|
+
? ["open", tmpPath]
|
|
188
|
+
: process.platform === "win32"
|
|
189
|
+
? ["cmd", "/c", "start", "", tmpPath]
|
|
190
|
+
: ["xdg-open", tmpPath];
|
|
191
|
+
|
|
192
|
+
Bun.spawn(openCmd, { stdout: "ignore", stderr: "ignore" }).unref();
|
|
193
|
+
|
|
194
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
195
|
+
this.ctx.chatContainer.addChild(
|
|
196
|
+
new Text(theme.fg("dim", `Opened flamegraph (${workProfile.sampleCount} samples)`), 1, 0),
|
|
197
|
+
);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
this.ctx.showError(`Failed to open profile: ${err instanceof Error ? err.message : String(err)}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this.ctx.ui.requestRender();
|
|
203
|
+
}
|
|
204
|
+
|
|
165
205
|
private async handleDumpReport(): Promise<void> {
|
|
166
206
|
const loader = new Loader(
|
|
167
207
|
this.ctx.ui,
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import * as fs from "node:fs/promises";
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
+
import type { WorkProfile } from "@oh-my-pi/pi-natives/work";
|
|
9
10
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
10
11
|
import type { CpuProfile, HeapSnapshot } from "./profiler";
|
|
11
12
|
import { collectSystemInfo, sanitizeEnv } from "./system-info";
|
|
@@ -42,6 +43,8 @@ export interface ReportBundleOptions {
|
|
|
42
43
|
cpuProfile?: CpuProfile;
|
|
43
44
|
/** Heap snapshot (for memory reports) */
|
|
44
45
|
heapSnapshot?: HeapSnapshot;
|
|
46
|
+
/** Work profile (for work scheduling reports) */
|
|
47
|
+
workProfile?: WorkProfile;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
export interface ReportBundleResult {
|
|
@@ -63,6 +66,9 @@ export interface ReportBundleResult {
|
|
|
63
66
|
* - profile.cpuprofile: CPU profile (performance report only)
|
|
64
67
|
* - profile.md: Markdown CPU profile (performance report only)
|
|
65
68
|
* - heap.heapsnapshot: Heap snapshot (memory report only)
|
|
69
|
+
* - work.folded: Work profile folded stacks (work report only)
|
|
70
|
+
* - work.md: Work profile summary (work report only)
|
|
71
|
+
* - work.svg: Work profile flamegraph (work report only)
|
|
66
72
|
*/
|
|
67
73
|
export async function createReportBundle(options: ReportBundleOptions): Promise<ReportBundleResult> {
|
|
68
74
|
const reportsDir = getReportsDir();
|
|
@@ -131,6 +137,18 @@ export async function createReportBundle(options: ReportBundleOptions): Promise<
|
|
|
131
137
|
files.push("heap.heapsnapshot");
|
|
132
138
|
}
|
|
133
139
|
|
|
140
|
+
// Work profile
|
|
141
|
+
if (options.workProfile) {
|
|
142
|
+
data["work.folded"] = options.workProfile.folded;
|
|
143
|
+
files.push("work.folded");
|
|
144
|
+
data["work.md"] = options.workProfile.summary;
|
|
145
|
+
files.push("work.md");
|
|
146
|
+
if (options.workProfile.svg) {
|
|
147
|
+
data["work.svg"] = options.workProfile.svg;
|
|
148
|
+
files.push("work.svg");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
134
152
|
// Write archive
|
|
135
153
|
await Bun.Archive.write(outputPath, data, { compress: "gzip" });
|
|
136
154
|
|
|
@@ -65,8 +65,6 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
let abortListener: (() => void) | undefined;
|
|
69
|
-
|
|
70
68
|
try {
|
|
71
69
|
const sessionKey = buildSessionKey(shell, prefix, snapshotPath, shellEnv, options?.sessionKey);
|
|
72
70
|
let shellSession = shellSessions.get(sessionKey);
|
|
@@ -75,19 +73,13 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
75
73
|
shellSessions.set(sessionKey, shellSession);
|
|
76
74
|
}
|
|
77
75
|
|
|
78
|
-
if (options?.signal) {
|
|
79
|
-
abortListener = () => {
|
|
80
|
-
shellSession?.abort();
|
|
81
|
-
};
|
|
82
|
-
options.signal.addEventListener("abort", abortListener, { once: true });
|
|
83
|
-
}
|
|
84
|
-
|
|
85
76
|
const result = await shellSession.run(
|
|
86
77
|
{
|
|
87
78
|
command: finalCommand,
|
|
88
79
|
cwd: options?.cwd,
|
|
89
80
|
env: options?.env,
|
|
90
81
|
timeoutMs: options?.timeout,
|
|
82
|
+
signal: options?.signal,
|
|
91
83
|
},
|
|
92
84
|
(err, chunk) => {
|
|
93
85
|
if (!err) {
|
|
@@ -127,9 +119,6 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
127
119
|
};
|
|
128
120
|
} finally {
|
|
129
121
|
await pendingChunks;
|
|
130
|
-
if (options?.signal && abortListener) {
|
|
131
|
-
options.signal.removeEventListener("abort", abortListener);
|
|
132
|
-
}
|
|
133
122
|
}
|
|
134
123
|
}
|
|
135
124
|
|
package/src/system-prompt.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import * as fs from "node:fs/promises";
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
-
import { getSystemInfo as getNativeSystemInfo, type SystemInfo
|
|
7
|
+
import { FileType, getSystemInfo as getNativeSystemInfo, glob, type SystemInfo } from "@oh-my-pi/pi-natives";
|
|
8
8
|
import { untilAborted } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import { $ } from "bun";
|
|
10
10
|
import chalk from "chalk";
|
|
@@ -184,10 +184,10 @@ async function scanProjectTreeWithGlob(root: string): Promise<ProjectTreeScan |
|
|
|
184
184
|
const timeoutSignal = AbortSignal.timeout(GLOB_TIMEOUT_MS);
|
|
185
185
|
try {
|
|
186
186
|
const result = await untilAborted(timeoutSignal, () =>
|
|
187
|
-
|
|
187
|
+
glob({
|
|
188
188
|
pattern: "**/*",
|
|
189
189
|
path: root,
|
|
190
|
-
fileType:
|
|
190
|
+
fileType: FileType.File,
|
|
191
191
|
}),
|
|
192
192
|
);
|
|
193
193
|
entries = result.matches.map(match => match.path).filter(entry => entry.length > 0);
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bash command normalizer -
|
|
2
|
+
* Bash command normalizer - extracts patterns that are better handled natively.
|
|
3
3
|
*
|
|
4
|
-
* Detects and
|
|
4
|
+
* Detects and extracts:
|
|
5
5
|
* - `| head -n N` / `| head -N` - extracted to headLines
|
|
6
6
|
* - `| tail -n N` / `| tail -N` - extracted to tailLines
|
|
7
|
-
* - `2>&1` - redundant since we merge stdout/stderr
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
export interface NormalizedCommand {
|
|
@@ -14,8 +13,6 @@ export interface NormalizedCommand {
|
|
|
14
13
|
headLines?: number;
|
|
15
14
|
/** Extracted tail line count, if any */
|
|
16
15
|
tailLines?: number;
|
|
17
|
-
/** Whether 2>&1 was stripped */
|
|
18
|
-
strippedRedirect: boolean;
|
|
19
16
|
}
|
|
20
17
|
|
|
21
18
|
/**
|
|
@@ -32,14 +29,6 @@ export interface NormalizedCommand {
|
|
|
32
29
|
*/
|
|
33
30
|
const TRAILING_HEAD_TAIL_PATTERN = /\|\s*(head|tail)\s+(?:-n\s*(\d+)|(-\d+))\s*$/;
|
|
34
31
|
|
|
35
|
-
/**
|
|
36
|
-
* Pattern to match 2>&1 redirection.
|
|
37
|
-
* Common variations:
|
|
38
|
-
* - `2>&1`
|
|
39
|
-
* - `2>&1 |` (before a pipe)
|
|
40
|
-
*/
|
|
41
|
-
const STDERR_REDIRECT_PATTERN = /\s*2>&1\s*/g;
|
|
42
|
-
|
|
43
32
|
/**
|
|
44
33
|
* Normalize a bash command by stripping patterns better handled natively.
|
|
45
34
|
*
|
|
@@ -52,13 +41,6 @@ export function normalizeBashCommand(command: string): NormalizedCommand {
|
|
|
52
41
|
let normalized = command;
|
|
53
42
|
let headLines: number | undefined;
|
|
54
43
|
let tailLines: number | undefined;
|
|
55
|
-
let strippedRedirect = false;
|
|
56
|
-
|
|
57
|
-
// Strip 2>&1 patterns (we merge streams already)
|
|
58
|
-
if (STDERR_REDIRECT_PATTERN.test(normalized)) {
|
|
59
|
-
normalized = normalized.replace(STDERR_REDIRECT_PATTERN, " ");
|
|
60
|
-
strippedRedirect = true;
|
|
61
|
-
}
|
|
62
44
|
|
|
63
45
|
// Extract trailing head/tail
|
|
64
46
|
const match = normalized.match(TRAILING_HEAD_TAIL_PATTERN);
|
|
@@ -82,7 +64,6 @@ export function normalizeBashCommand(command: string): NormalizedCommand {
|
|
|
82
64
|
command: normalized,
|
|
83
65
|
headLines,
|
|
84
66
|
tailLines,
|
|
85
|
-
strippedRedirect,
|
|
86
67
|
};
|
|
87
68
|
}
|
|
88
69
|
|
package/src/tools/find.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
4
|
-
import { type
|
|
4
|
+
import { FileType, type GlobMatch, glob } from "@oh-my-pi/pi-natives";
|
|
5
5
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
6
6
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { isEnoent, untilAborted } from "@oh-my-pi/pi-utils";
|
|
@@ -252,7 +252,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
252
252
|
throw new ToolError(`Path is not a directory: ${searchPath}`);
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
let matches: Awaited<ReturnType<typeof
|
|
255
|
+
let matches: Awaited<ReturnType<typeof glob>>["matches"];
|
|
256
256
|
const onUpdateMatches: string[] = [];
|
|
257
257
|
const updateIntervalMs = 200;
|
|
258
258
|
let lastUpdate = 0;
|
|
@@ -273,11 +273,11 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
273
273
|
});
|
|
274
274
|
};
|
|
275
275
|
const onMatch = onUpdate
|
|
276
|
-
? (match:
|
|
276
|
+
? (match: GlobMatch | null) => {
|
|
277
277
|
if (signal?.aborted || !match) return;
|
|
278
278
|
let relativePath = match.path;
|
|
279
279
|
if (!relativePath) return;
|
|
280
|
-
if (match.fileType ===
|
|
280
|
+
if (match.fileType === FileType.Dir && !relativePath.endsWith("/")) {
|
|
281
281
|
relativePath += "/";
|
|
282
282
|
}
|
|
283
283
|
onUpdateMatches.push(relativePath);
|
|
@@ -288,11 +288,11 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
288
288
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
289
289
|
try {
|
|
290
290
|
const result = await untilAborted(combinedSignal, () =>
|
|
291
|
-
|
|
291
|
+
glob(
|
|
292
292
|
{
|
|
293
293
|
pattern: globPattern,
|
|
294
294
|
path: searchPath,
|
|
295
|
-
fileType:
|
|
295
|
+
fileType: FileType.File,
|
|
296
296
|
hidden: includeHidden,
|
|
297
297
|
maxResults: effectiveLimit,
|
|
298
298
|
sortByMtime: true,
|
|
@@ -328,7 +328,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
328
328
|
const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\");
|
|
329
329
|
let relativePath = line;
|
|
330
330
|
|
|
331
|
-
const isDirectory = match.fileType ===
|
|
331
|
+
const isDirectory = match.fileType === FileType.Dir;
|
|
332
332
|
|
|
333
333
|
if ((isDirectory || hadTrailingSlash) && !relativePath.endsWith("/")) {
|
|
334
334
|
relativePath += "/";
|
package/src/tools/read.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
5
5
|
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
6
|
-
import {
|
|
6
|
+
import { FileType, glob } from "@oh-my-pi/pi-natives";
|
|
7
7
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
8
8
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
9
9
|
import { ptree, untilAborted } from "@oh-my-pi/pi-utils";
|
|
@@ -361,10 +361,10 @@ async function listCandidateFiles(
|
|
|
361
361
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
362
362
|
try {
|
|
363
363
|
const result = await untilAborted(combinedSignal, () =>
|
|
364
|
-
|
|
364
|
+
glob({
|
|
365
365
|
pattern: "**/*",
|
|
366
366
|
path: searchRoot,
|
|
367
|
-
fileType:
|
|
367
|
+
fileType: FileType.File,
|
|
368
368
|
hidden: true,
|
|
369
369
|
}),
|
|
370
370
|
);
|