@oh-my-pi/pi-coding-agent 9.8.0 → 10.0.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 +12 -0
- package/package.json +7 -7
- package/src/cli/args.ts +1 -0
- package/src/cli/shell-cli.ts +174 -0
- package/src/main.ts +12 -0
- package/src/tools/find.ts +35 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [10.0.0] - 2026-02-01
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added `shell` subcommand for interactive shell console testing with brush-core
|
|
9
|
+
- Added `--cwd` / `-C` option to set working directory for shell commands
|
|
10
|
+
- Added `--timeout` / `-t` option to configure per-command timeout in milliseconds
|
|
11
|
+
- Added `--no-snapshot` option to skip sourcing snapshot from user shell
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- `find` now returns a single match when given a file path instead of failing with "not a directory"
|
|
16
|
+
|
|
5
17
|
## [9.8.0] - 2026-02-01
|
|
6
18
|
### Breaking Changes
|
|
7
19
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.0.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.0.0",
|
|
83
|
+
"@oh-my-pi/pi-agent-core": "10.0.0",
|
|
84
|
+
"@oh-my-pi/pi-ai": "10.0.0",
|
|
85
|
+
"@oh-my-pi/pi-natives": "10.0.0",
|
|
86
|
+
"@oh-my-pi/pi-tui": "10.0.0",
|
|
87
|
+
"@oh-my-pi/pi-utils": "10.0.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/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/tools/find.ts
CHANGED
|
@@ -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,6 +237,17 @@ 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
|
}
|