@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 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": "9.8.0",
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": "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",
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
  }