@oh-my-pi/pi-coding-agent 9.8.0 → 10.2.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 +18 -0
- package/package.json +7 -7
- package/src/cli/args.ts +1 -0
- package/src/cli/shell-cli.ts +174 -0
- 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/main.ts +12 -0
- package/src/system-prompt.ts +3 -3
- package/src/tools/find.ts +42 -7
- package/src/tools/read.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [10.1.0] - 2026-02-01
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added work scheduling profiler to debug menu for analyzing CPU scheduling patterns over the last 30 seconds
|
|
9
|
+
- Added support for work profile data in report bundles including folded stacks, summary, and flamegraph visualization
|
|
10
|
+
|
|
11
|
+
## [10.0.0] - 2026-02-01
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added `shell` subcommand for interactive shell console testing with brush-core
|
|
15
|
+
- Added `--cwd` / `-C` option to set working directory for shell commands
|
|
16
|
+
- Added `--timeout` / `-t` option to configure per-command timeout in milliseconds
|
|
17
|
+
- Added `--no-snapshot` option to skip sourcing snapshot from user shell
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- `find` now returns a single match when given a file path instead of failing with "not a directory"
|
|
22
|
+
|
|
5
23
|
## [9.8.0] - 2026-02-01
|
|
6
24
|
### Breaking Changes
|
|
7
25
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.2.0",
|
|
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": "
|
|
83
|
-
"@oh-my-pi/pi-agent-core": "
|
|
84
|
-
"@oh-my-pi/pi-ai": "
|
|
85
|
-
"@oh-my-pi/pi-natives": "
|
|
86
|
-
"@oh-my-pi/pi-tui": "
|
|
87
|
-
"@oh-my-pi/pi-utils": "
|
|
82
|
+
"@oh-my-pi/omp-stats": "10.2.0",
|
|
83
|
+
"@oh-my-pi/pi-agent-core": "10.2.0",
|
|
84
|
+
"@oh-my-pi/pi-ai": "10.2.0",
|
|
85
|
+
"@oh-my-pi/pi-natives": "10.2.0",
|
|
86
|
+
"@oh-my-pi/pi-tui": "10.2.0",
|
|
87
|
+
"@oh-my-pi/pi-utils": "10.2.0",
|
|
88
88
|
"@openai/agents": "^0.4.4",
|
|
89
89
|
"@sinclair/typebox": "^0.34.48",
|
|
90
90
|
"ajv": "^8.17.1",
|
package/src/cli/args.ts
CHANGED
|
@@ -191,6 +191,7 @@ ${chalk.bold("Subcommands:")}
|
|
|
191
191
|
update Check for and install updates
|
|
192
192
|
config Manage configuration settings
|
|
193
193
|
setup Install dependencies for optional features
|
|
194
|
+
shell Interactive shell console (brush-core test)
|
|
194
195
|
|
|
195
196
|
${chalk.bold("Options:")}
|
|
196
197
|
--model <pattern> Model to use (fuzzy match: "opus", "gpt-5.2", or "p-openai/gpt-5.2")
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell CLI command handlers.
|
|
3
|
+
*
|
|
4
|
+
* Handles `omp shell` subcommand for testing the native brush-core shell.
|
|
5
|
+
*/
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import { Shell } from "@oh-my-pi/pi-natives";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import { APP_NAME } from "../config";
|
|
11
|
+
import { Settings } from "../config/settings";
|
|
12
|
+
import { getOrCreateSnapshot } from "../utils/shell-snapshot";
|
|
13
|
+
|
|
14
|
+
export interface ShellCommandArgs {
|
|
15
|
+
cwd?: string;
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
noSnapshot?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseShellArgs(args: string[]): ShellCommandArgs | undefined {
|
|
21
|
+
if (args.length === 0 || args[0] !== "shell") {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const result: ShellCommandArgs = {};
|
|
26
|
+
|
|
27
|
+
for (let i = 1; i < args.length; i++) {
|
|
28
|
+
const arg = args[i];
|
|
29
|
+
if (arg === "--cwd" || arg === "-C") {
|
|
30
|
+
result.cwd = args[++i];
|
|
31
|
+
} else if (arg === "--timeout" || arg === "-t") {
|
|
32
|
+
const parsed = Number.parseInt(args[++i], 10);
|
|
33
|
+
if (Number.isFinite(parsed)) {
|
|
34
|
+
result.timeoutMs = parsed;
|
|
35
|
+
}
|
|
36
|
+
} else if (arg === "--no-snapshot") {
|
|
37
|
+
result.noSnapshot = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function runShellCommand(cmd: ShellCommandArgs): Promise<void> {
|
|
45
|
+
if (!process.stdin.isTTY) {
|
|
46
|
+
process.stderr.write("Error: shell console requires an interactive TTY.\n");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const cwd = cmd.cwd ? path.resolve(cmd.cwd) : process.cwd();
|
|
51
|
+
const settings = await Settings.init({ cwd });
|
|
52
|
+
const { shell, env: shellEnv } = settings.getShellConfig();
|
|
53
|
+
const snapshotPath = cmd.noSnapshot || !shell.includes("bash") ? null : await getOrCreateSnapshot(shell, shellEnv);
|
|
54
|
+
const shellSession = new Shell({ sessionEnv: shellEnv, snapshotPath: snapshotPath ?? undefined });
|
|
55
|
+
|
|
56
|
+
let active = false;
|
|
57
|
+
let lastChar: string | null = null;
|
|
58
|
+
|
|
59
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
60
|
+
const prompt = chalk.cyan(`${APP_NAME} shell> `);
|
|
61
|
+
|
|
62
|
+
const printHelp = () => {
|
|
63
|
+
process.stdout.write(
|
|
64
|
+
`${chalk.bold("Shell Console Commands")}
|
|
65
|
+
|
|
66
|
+
` +
|
|
67
|
+
`${chalk.bold("Special Commands:")}
|
|
68
|
+
.help Show this help
|
|
69
|
+
.exit, exit Exit the console
|
|
70
|
+
|
|
71
|
+
` +
|
|
72
|
+
`${chalk.bold("Options:")}
|
|
73
|
+
--cwd, -C <path> Set working directory for commands
|
|
74
|
+
--timeout, -t <ms> Timeout per command in milliseconds
|
|
75
|
+
--no-snapshot Skip sourcing snapshot from user shell
|
|
76
|
+
|
|
77
|
+
` +
|
|
78
|
+
`${chalk.bold("Notes:")}
|
|
79
|
+
Runs in a persistent brush-core shell session.
|
|
80
|
+
Variables and functions defined in one command persist for the next.
|
|
81
|
+
|
|
82
|
+
`,
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const interruptHandler = () => {
|
|
87
|
+
if (active) {
|
|
88
|
+
shellSession.abort();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
rl.close();
|
|
92
|
+
process.exit(0);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
process.on("SIGINT", interruptHandler);
|
|
96
|
+
process.stdout.write(chalk.dim("Type .help for commands.\n"));
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
while (true) {
|
|
100
|
+
const line = (await rl.question(prompt)).trim();
|
|
101
|
+
if (!line) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (line === ".help") {
|
|
105
|
+
printHelp();
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (line === ".exit" || line === "exit" || line === "quit") {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
active = true;
|
|
113
|
+
lastChar = null;
|
|
114
|
+
try {
|
|
115
|
+
const result = await shellSession.run(
|
|
116
|
+
{
|
|
117
|
+
command: line,
|
|
118
|
+
cwd,
|
|
119
|
+
timeoutMs: cmd.timeoutMs,
|
|
120
|
+
},
|
|
121
|
+
(err, chunk) => {
|
|
122
|
+
if (err) {
|
|
123
|
+
process.stderr.write(`${err.message}\n`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (chunk.length > 0) {
|
|
127
|
+
lastChar = chunk[chunk.length - 1] ?? null;
|
|
128
|
+
}
|
|
129
|
+
process.stdout.write(chunk);
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (lastChar && lastChar !== "\n") {
|
|
134
|
+
process.stdout.write("\n");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (result.timedOut) {
|
|
138
|
+
process.stderr.write(chalk.yellow("Command timed out.\n"));
|
|
139
|
+
} else if (result.cancelled) {
|
|
140
|
+
process.stderr.write(chalk.yellow("Command cancelled.\n"));
|
|
141
|
+
} else if (result.exitCode !== 0 && result.exitCode !== undefined) {
|
|
142
|
+
process.stderr.write(chalk.yellow(`Exit code: ${result.exitCode}\n`));
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {
|
|
145
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
146
|
+
process.stderr.write(chalk.red(`Error: ${message}\n`));
|
|
147
|
+
} finally {
|
|
148
|
+
active = false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} finally {
|
|
152
|
+
process.off("SIGINT", interruptHandler);
|
|
153
|
+
rl.close();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function printShellHelp(): void {
|
|
158
|
+
process.stdout.write(`${chalk.bold(`${APP_NAME} shell`)} - Interactive shell console for testing
|
|
159
|
+
|
|
160
|
+
${chalk.bold("Usage:")}
|
|
161
|
+
${APP_NAME} shell [options]
|
|
162
|
+
|
|
163
|
+
${chalk.bold("Options:")}
|
|
164
|
+
--cwd, -C <path> Set working directory for commands
|
|
165
|
+
--timeout, -t <ms> Timeout per command in milliseconds
|
|
166
|
+
--no-snapshot Skip sourcing snapshot from user shell
|
|
167
|
+
-h, --help Show this help
|
|
168
|
+
|
|
169
|
+
${chalk.bold("Examples:")}
|
|
170
|
+
${APP_NAME} shell
|
|
171
|
+
${APP_NAME} shell --cwd ./tmp
|
|
172
|
+
${APP_NAME} shell --timeout 2000
|
|
173
|
+
`);
|
|
174
|
+
}
|
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/main.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { listModels } from "./cli/list-models";
|
|
|
20
20
|
import { parsePluginArgs, printPluginHelp, runPluginCommand } from "./cli/plugin-cli";
|
|
21
21
|
import { selectSession } from "./cli/session-picker";
|
|
22
22
|
import { parseSetupArgs, printSetupHelp, runSetupCommand } from "./cli/setup-cli";
|
|
23
|
+
import { parseShellArgs, printShellHelp, runShellCommand } from "./cli/shell-cli";
|
|
23
24
|
import { parseStatsArgs, printStatsHelp, runStatsCommand } from "./cli/stats-cli";
|
|
24
25
|
import { parseUpdateArgs, printUpdateHelp, runUpdateCommand } from "./cli/update-cli";
|
|
25
26
|
import { runCommitCommand } from "./commit";
|
|
@@ -558,6 +559,17 @@ export async function main(args: string[]) {
|
|
|
558
559
|
return;
|
|
559
560
|
}
|
|
560
561
|
|
|
562
|
+
// Handle shell subcommand (for testing brush-core shell)
|
|
563
|
+
const shellCmd = parseShellArgs(args);
|
|
564
|
+
if (shellCmd) {
|
|
565
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
566
|
+
printShellHelp();
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
await runShellCommand(shellCmd);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
561
573
|
// Handle commit subcommand
|
|
562
574
|
const commitCmd = parseCommitArgs(args);
|
|
563
575
|
if (commitCmd) {
|
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);
|
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";
|
|
@@ -75,6 +75,11 @@ function parsePatternPath(pattern: string): { basePath: string; globPattern: str
|
|
|
75
75
|
return { basePath, globPattern };
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function hasGlobChars(pattern: string): boolean {
|
|
79
|
+
const globChars = ["*", "?", "[", "{"];
|
|
80
|
+
return globChars.some(char => pattern.includes(char));
|
|
81
|
+
}
|
|
82
|
+
|
|
78
83
|
export interface FindToolDetails {
|
|
79
84
|
truncation?: TruncationResult;
|
|
80
85
|
resultLimitReached?: number;
|
|
@@ -94,6 +99,10 @@ export interface FindToolDetails {
|
|
|
94
99
|
export interface FindOperations {
|
|
95
100
|
/** Check if path exists */
|
|
96
101
|
exists: (absolutePath: string) => Promise<boolean> | boolean;
|
|
102
|
+
/** Optional stat for distinguishing files vs directories. */
|
|
103
|
+
stat?: (
|
|
104
|
+
absolutePath: string,
|
|
105
|
+
) => Promise<{ isFile(): boolean; isDirectory(): boolean }> | { isFile(): boolean; isDirectory(): boolean };
|
|
97
106
|
/** Find files matching glob pattern. Returns relative paths. */
|
|
98
107
|
glob: (pattern: string, cwd: string, options: { ignore: string[]; limit: number }) => Promise<string[]> | string[];
|
|
99
108
|
}
|
|
@@ -136,6 +145,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
136
145
|
throw new ToolError("Pattern must not be empty");
|
|
137
146
|
}
|
|
138
147
|
|
|
148
|
+
const hasGlob = hasGlobChars(normalizedPattern);
|
|
139
149
|
const { basePath, globPattern } = parsePatternPath(normalizedPattern);
|
|
140
150
|
const searchPath = resolveToCwd(basePath, this.session.cwd);
|
|
141
151
|
|
|
@@ -161,6 +171,20 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
161
171
|
throw new ToolError(`Path not found: ${searchPath}`);
|
|
162
172
|
}
|
|
163
173
|
|
|
174
|
+
if (!hasGlob && this.customOps.stat) {
|
|
175
|
+
const stat = await this.customOps.stat(searchPath);
|
|
176
|
+
if (stat.isFile()) {
|
|
177
|
+
const files = [scopePath];
|
|
178
|
+
const details: FindToolDetails = {
|
|
179
|
+
scopePath,
|
|
180
|
+
fileCount: 1,
|
|
181
|
+
files,
|
|
182
|
+
truncated: false,
|
|
183
|
+
};
|
|
184
|
+
return toolResult(details).text(files.join("\n")).done();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
164
188
|
const results = await this.customOps.glob(globPattern, searchPath, {
|
|
165
189
|
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
166
190
|
limit: effectiveLimit,
|
|
@@ -213,11 +237,22 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
213
237
|
}
|
|
214
238
|
throw err;
|
|
215
239
|
}
|
|
240
|
+
|
|
241
|
+
if (!hasGlob && searchStat.isFile()) {
|
|
242
|
+
const files = [scopePath];
|
|
243
|
+
const details: FindToolDetails = {
|
|
244
|
+
scopePath,
|
|
245
|
+
fileCount: 1,
|
|
246
|
+
files,
|
|
247
|
+
truncated: false,
|
|
248
|
+
};
|
|
249
|
+
return toolResult(details).text(files.join("\n")).done();
|
|
250
|
+
}
|
|
216
251
|
if (!searchStat.isDirectory()) {
|
|
217
252
|
throw new ToolError(`Path is not a directory: ${searchPath}`);
|
|
218
253
|
}
|
|
219
254
|
|
|
220
|
-
let matches: Awaited<ReturnType<typeof
|
|
255
|
+
let matches: Awaited<ReturnType<typeof glob>>["matches"];
|
|
221
256
|
const onUpdateMatches: string[] = [];
|
|
222
257
|
const updateIntervalMs = 200;
|
|
223
258
|
let lastUpdate = 0;
|
|
@@ -238,11 +273,11 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
238
273
|
});
|
|
239
274
|
};
|
|
240
275
|
const onMatch = onUpdate
|
|
241
|
-
? (match:
|
|
276
|
+
? (match: GlobMatch | null) => {
|
|
242
277
|
if (signal?.aborted || !match) return;
|
|
243
278
|
let relativePath = match.path;
|
|
244
279
|
if (!relativePath) return;
|
|
245
|
-
if (match.fileType ===
|
|
280
|
+
if (match.fileType === FileType.Dir && !relativePath.endsWith("/")) {
|
|
246
281
|
relativePath += "/";
|
|
247
282
|
}
|
|
248
283
|
onUpdateMatches.push(relativePath);
|
|
@@ -253,11 +288,11 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
253
288
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
254
289
|
try {
|
|
255
290
|
const result = await untilAborted(combinedSignal, () =>
|
|
256
|
-
|
|
291
|
+
glob(
|
|
257
292
|
{
|
|
258
293
|
pattern: globPattern,
|
|
259
294
|
path: searchPath,
|
|
260
|
-
fileType:
|
|
295
|
+
fileType: FileType.File,
|
|
261
296
|
hidden: includeHidden,
|
|
262
297
|
maxResults: effectiveLimit,
|
|
263
298
|
sortByMtime: true,
|
|
@@ -293,7 +328,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
293
328
|
const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\");
|
|
294
329
|
let relativePath = line;
|
|
295
330
|
|
|
296
|
-
const isDirectory = match.fileType ===
|
|
331
|
+
const isDirectory = match.fileType === FileType.Dir;
|
|
297
332
|
|
|
298
333
|
if ((isDirectory || hadTrailingSlash) && !relativePath.endsWith("/")) {
|
|
299
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
|
);
|