@oh-my-pi/pi-coding-agent 9.1.1 → 9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "9.1.1",
3
+ "version": "9.2.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": "bun build --compile ./src/cli.ts --outfile dist/omp",
77
+ "build:binary": "cd ../.. && bun build --compile --define OMP_COMPILED=true --root . ./packages/coding-agent/src/cli.ts ./packages/natives/src/grep/worker.ts ./packages/natives/src/html/worker.ts ./packages/natives/src/image/worker.ts --outfile packages/coding-agent/dist/omp",
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.1.1",
83
- "@oh-my-pi/pi-agent-core": "9.1.1",
84
- "@oh-my-pi/pi-ai": "9.1.1",
85
- "@oh-my-pi/pi-natives": "9.1.1",
86
- "@oh-my-pi/pi-tui": "9.1.1",
87
- "@oh-my-pi/pi-utils": "9.1.1",
82
+ "@oh-my-pi/omp-stats": "9.2.0",
83
+ "@oh-my-pi/pi-agent-core": "9.2.0",
84
+ "@oh-my-pi/pi-ai": "9.2.0",
85
+ "@oh-my-pi/pi-natives": "9.2.0",
86
+ "@oh-my-pi/pi-tui": "9.2.0",
87
+ "@oh-my-pi/pi-utils": "9.2.0",
88
88
  "@openai/agents": "^0.4.4",
89
89
  "@sinclair/typebox": "^0.34.48",
90
90
  "ajv": "^8.17.1",
@@ -97,7 +97,6 @@
97
97
  "nanoid": "^5.1.6",
98
98
  "node-html-parser": "^7.0.2",
99
99
  "smol-toml": "^1.6.0",
100
- "strip-ansi": "^7.1.2",
101
100
  "zod": "^4.3.6"
102
101
  },
103
102
  "devDependencies": {
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Grep CLI command handlers.
3
+ *
4
+ * Handles `omp grep` subcommand for testing grep tool on Windows.
5
+ */
6
+ import * as path from "node:path";
7
+ import { grep } from "@oh-my-pi/pi-natives";
8
+ import chalk from "chalk";
9
+ import { APP_NAME } from "../config";
10
+
11
+ export interface GrepCommandArgs {
12
+ pattern: string;
13
+ path: string;
14
+ glob?: string;
15
+ limit: number;
16
+ context: number;
17
+ mode: "content" | "filesWithMatches" | "count";
18
+ }
19
+
20
+ /**
21
+ * Parse grep subcommand arguments.
22
+ * Returns undefined if not a grep command.
23
+ */
24
+ export function parseGrepArgs(args: string[]): GrepCommandArgs | undefined {
25
+ if (args.length === 0 || args[0] !== "grep") {
26
+ return undefined;
27
+ }
28
+
29
+ const result: GrepCommandArgs = {
30
+ pattern: "",
31
+ path: ".",
32
+ limit: 20,
33
+ context: 2,
34
+ mode: "content",
35
+ };
36
+
37
+ const positional: string[] = [];
38
+
39
+ for (let i = 1; i < args.length; i++) {
40
+ const arg = args[i];
41
+ if (arg === "--glob" || arg === "-g") {
42
+ result.glob = args[++i];
43
+ } else if (arg === "--limit" || arg === "-l") {
44
+ result.limit = parseInt(args[++i], 10);
45
+ } else if (arg === "--context" || arg === "-C") {
46
+ result.context = parseInt(args[++i], 10);
47
+ } else if (arg === "--files" || arg === "-f") {
48
+ result.mode = "filesWithMatches";
49
+ } else if (arg === "--count" || arg === "-c") {
50
+ result.mode = "count";
51
+ } else if (!arg.startsWith("-")) {
52
+ positional.push(arg);
53
+ }
54
+ }
55
+
56
+ if (positional.length >= 1) {
57
+ result.pattern = positional[0];
58
+ }
59
+ if (positional.length >= 2) {
60
+ result.path = positional[1];
61
+ }
62
+
63
+ return result;
64
+ }
65
+
66
+ export async function runGrepCommand(cmd: GrepCommandArgs): Promise<void> {
67
+ if (!cmd.pattern) {
68
+ console.error(chalk.red("Error: Pattern is required"));
69
+ process.exit(1);
70
+ }
71
+
72
+ const searchPath = path.resolve(cmd.path);
73
+ console.log(chalk.dim(`Searching in: ${searchPath}`));
74
+ console.log(chalk.dim(`Pattern: ${cmd.pattern}`));
75
+ console.log(chalk.dim(`Mode: ${cmd.mode}, Limit: ${cmd.limit}, Context: ${cmd.context}`));
76
+
77
+ console.log("");
78
+
79
+ try {
80
+ const result = await grep({
81
+ pattern: cmd.pattern,
82
+ path: searchPath,
83
+ glob: cmd.glob,
84
+ mode: cmd.mode,
85
+ maxCount: cmd.limit,
86
+ context: cmd.mode === "content" ? cmd.context : undefined,
87
+ hidden: true,
88
+ });
89
+
90
+ console.log(chalk.green(`Total matches: ${result.totalMatches}`));
91
+ console.log(chalk.green(`Files with matches: ${result.filesWithMatches}`));
92
+ console.log(chalk.green(`Files searched: ${result.filesSearched}`));
93
+ if (result.limitReached) {
94
+ console.log(chalk.yellow(`Limit reached: true`));
95
+ }
96
+ console.log("");
97
+
98
+ for (const match of result.matches) {
99
+ const displayPath = match.path.replace(/\\/g, "/");
100
+
101
+ if (cmd.mode === "content") {
102
+ if (match.contextBefore) {
103
+ for (const ctx of match.contextBefore) {
104
+ console.log(chalk.dim(`${displayPath}-${ctx.lineNumber}- ${ctx.line}`));
105
+ }
106
+ }
107
+ console.log(`${chalk.cyan(displayPath)}:${chalk.yellow(String(match.lineNumber))}: ${match.line}`);
108
+ if (match.contextAfter) {
109
+ for (const ctx of match.contextAfter) {
110
+ console.log(chalk.dim(`${displayPath}-${ctx.lineNumber}- ${ctx.line}`));
111
+ }
112
+ }
113
+ console.log("");
114
+ } else if (cmd.mode === "count") {
115
+ console.log(`${chalk.cyan(displayPath)}: ${match.matchCount ?? 0} matches`);
116
+ } else {
117
+ console.log(chalk.cyan(displayPath));
118
+ }
119
+ }
120
+ } catch (err) {
121
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
122
+ process.exit(1);
123
+ }
124
+ }
125
+
126
+ export function printGrepHelp(): void {
127
+ console.log(`${chalk.bold(`${APP_NAME} grep`)} - Test grep tool
128
+
129
+ ${chalk.bold("Usage:")}
130
+ ${APP_NAME} grep <pattern> [path] [options]
131
+
132
+ ${chalk.bold("Arguments:")}
133
+ pattern Regex pattern to search for
134
+ path Directory or file to search (default: .)
135
+
136
+ ${chalk.bold("Options:")}
137
+ -g, --glob <pattern> Filter files by glob pattern
138
+ -l, --limit <n> Max matches (default: 20)
139
+ -C, --context <n> Context lines (default: 2)
140
+ -f, --files Output file names only
141
+ -c, --count Output match counts per file
142
+ -h, --help Show this help
143
+
144
+ ${chalk.bold("Environment:")}
145
+ OMP_GREP_WORKERS=0 Disable worker pool (use single-threaded mode)
146
+
147
+ ${chalk.bold("Examples:")}
148
+ ${APP_NAME} grep "import" src/
149
+ ${APP_NAME} grep "TODO" . --glob "*.ts"
150
+ ${APP_NAME} grep "function" --files
151
+ `);
152
+ }
@@ -90,10 +90,12 @@ export interface ExaSettings {
90
90
 
91
91
  export type WebSearchProviderOption = "auto" | "exa" | "perplexity" | "anthropic";
92
92
  export type ImageProviderOption = "auto" | "gemini" | "openrouter";
93
+ export type KimiApiFormatOption = "openai" | "anthropic";
93
94
 
94
95
  export interface ProviderSettings {
95
96
  webSearch?: WebSearchProviderOption; // default: "auto" (exa > perplexity > anthropic)
96
97
  image?: ImageProviderOption; // default: "auto" (openrouter > gemini)
98
+ kimiApiFormat?: KimiApiFormatOption; // default: "anthropic" (use Anthropic-compatible API for Kimi, more stable)
97
99
  }
98
100
 
99
101
  export interface BashInterceptorRule {
@@ -1349,6 +1351,19 @@ export class SettingsManager {
1349
1351
  await this.save();
1350
1352
  }
1351
1353
 
1354
+ getKimiApiFormat(): KimiApiFormatOption {
1355
+ return this.settings.providers?.kimiApiFormat ?? "anthropic";
1356
+ }
1357
+
1358
+ async setKimiApiFormat(format: KimiApiFormatOption): Promise<void> {
1359
+ if (!this.globalSettings.providers) {
1360
+ this.globalSettings.providers = {};
1361
+ }
1362
+ this.globalSettings.providers.kimiApiFormat = format;
1363
+ this.markModified("providers", "kimiApiFormat");
1364
+ await this.save();
1365
+ }
1366
+
1352
1367
  getBashInterceptorEnabled(): boolean {
1353
1368
  return this.settings.bashInterceptor?.enabled ?? DEFAULT_BASH_INTERCEPTOR_SETTINGS.enabled;
1354
1369
  }
package/src/lsp/render.ts CHANGED
@@ -306,6 +306,8 @@ function highlightCode(codeText: string, language: string, theme: Theme): string
306
306
  type: theme.getFgAnsi("syntaxType"),
307
307
  operator: theme.getFgAnsi("syntaxOperator"),
308
308
  punctuation: theme.getFgAnsi("syntaxPunctuation"),
309
+ inserted: theme.getFgAnsi("toolDiffAdded"),
310
+ deleted: theme.getFgAnsi("toolDiffRemoved"),
309
311
  };
310
312
  return nativeHighlightCode(codeText, validLang, colors).split("\n");
311
313
  } catch {
package/src/main.ts CHANGED
@@ -14,6 +14,7 @@ import chalk from "chalk";
14
14
  import { type Args, parseArgs, printHelp } from "./cli/args";
15
15
  import { parseConfigArgs, printConfigHelp, runConfigCommand } from "./cli/config-cli";
16
16
  import { processFileArguments } from "./cli/file-processor";
17
+ import { parseGrepArgs, printGrepHelp, runGrepCommand } from "./cli/grep-cli";
17
18
  import { parseJupyterArgs, printJupyterHelp, runJupyterCommand } from "./cli/jupyter-cli";
18
19
  import { listModels } from "./cli/list-models";
19
20
  import { parsePluginArgs, printPluginHelp, runPluginCommand } from "./cli/plugin-cli";
@@ -553,6 +554,17 @@ export async function main(args: string[]) {
553
554
  return;
554
555
  }
555
556
 
557
+ // Handle grep subcommand (for testing grep tool)
558
+ const grepCmd = parseGrepArgs(args);
559
+ if (grepCmd) {
560
+ if (args.includes("--help") || args.includes("-h")) {
561
+ printGrepHelp();
562
+ return;
563
+ }
564
+ await runGrepCommand(grepCmd);
565
+ return;
566
+ }
567
+
556
568
  // Handle commit subcommand
557
569
  const commitCmd = parseCommitArgs(args);
558
570
  if (commitCmd) {
@@ -2,7 +2,6 @@
2
2
  * Component for displaying bash command execution with streaming output.
3
3
  */
4
4
  import { Container, Loader, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
5
- import stripAnsi from "strip-ansi";
6
5
  import { getSymbolTheme, theme } from "../../modes/theme/theme";
7
6
  import type { TruncationMeta } from "../../tools/output-meta";
8
7
  import { formatSize } from "../../tools/truncate";
@@ -188,7 +187,7 @@ export class BashExecutionComponent extends Container {
188
187
  private normalizeOutput(text: string): string {
189
188
  // Strip ANSI codes and normalize line endings
190
189
  // Note: binary data is already sanitized in tui-renderer.ts executeBashCommand
191
- return stripAnsi(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
190
+ return Bun.stripANSI(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
192
191
  }
193
192
 
194
193
  private setOutput(output: string): void {
@@ -3,7 +3,6 @@
3
3
  * Shares the same kernel session as the agent's Python tool.
4
4
  */
5
5
  import { Container, Loader, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
6
- import stripAnsi from "strip-ansi";
7
6
  import { getSymbolTheme, highlightCode, theme } from "../../modes/theme/theme";
8
7
  import type { TruncationMeta } from "../../tools/output-meta";
9
8
  import { formatSize } from "../../tools/truncate";
@@ -172,7 +171,7 @@ export class PythonExecutionComponent extends Container {
172
171
  }
173
172
 
174
173
  private normalizeOutput(text: string): string {
175
- return stripAnsi(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
174
+ return Bun.stripANSI(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
176
175
  }
177
176
 
178
177
  private setOutput(output: string): void {
@@ -11,6 +11,7 @@ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
11
11
  import { TERMINAL_INFO } from "@oh-my-pi/pi-tui";
12
12
  import type {
13
13
  ImageProviderOption,
14
+ KimiApiFormatOption,
14
15
  NotificationMethod,
15
16
  PythonKernelMode,
16
17
  PythonToolMode,
@@ -416,6 +417,19 @@ export const SETTINGS_DEFS: SettingDef[] = [
416
417
  { value: "openrouter", label: "OpenRouter", description: "Use OpenRouter (requires OPENROUTER_API_KEY)" },
417
418
  ],
418
419
  },
420
+ {
421
+ id: "kimiApiFormat",
422
+ tab: "tools",
423
+ type: "submenu",
424
+ label: "Kimi API format",
425
+ description: "API format for Kimi Code provider",
426
+ get: sm => sm.getKimiApiFormat(),
427
+ set: (sm, v) => sm.setKimiApiFormat(v as KimiApiFormatOption),
428
+ getOptions: () => [
429
+ { value: "openai", label: "OpenAI", description: "Use OpenAI-compatible API (api.kimi.com)" },
430
+ { value: "anthropic", label: "Anthropic", description: "Use Anthropic-compatible API (api.moonshot.ai)" },
431
+ ],
432
+ },
419
433
 
420
434
  // ═══════════════════════════════════════════════════════════════════════════
421
435
  // Display tab - Visual/UI settings
@@ -2049,6 +2049,8 @@ function getHighlightColors(t: Theme): NativeHighlightColors {
2049
2049
  type: t.getFgAnsi("syntaxType"),
2050
2050
  operator: t.getFgAnsi("syntaxOperator"),
2051
2051
  punctuation: t.getFgAnsi("syntaxPunctuation"),
2052
+ inserted: t.getFgAnsi("toolDiffAdded"),
2053
+ deleted: t.getFgAnsi("toolDiffRemoved"),
2052
2054
  };
2053
2055
  }
2054
2056
  return cachedHighlightColors;
@@ -3,7 +3,8 @@
3
3
  Fast file pattern matching that works with any codebase size.
4
4
 
5
5
  <instruction>
6
- - Supports glob patterns like `**/*.js` or `src/**/*.ts`
6
+ - Pattern includes the search path: `src/**/*.ts`, `lib/*.json`, `**/*.md`
7
+ - Simple patterns like `*.ts` automatically search recursively from cwd
7
8
  - Includes hidden files by default (use `hidden: false` to exclude)
8
9
  - Speculatively perform multiple searches in parallel when potentially useful
9
10
  </instruction>
package/src/sdk.ts CHANGED
@@ -1115,6 +1115,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1115
1115
  followUpMode: settingsManager.getFollowUpMode(),
1116
1116
  interruptMode: settingsManager.getInterruptMode(),
1117
1117
  thinkingBudgets: settingsManager.getThinkingBudgets(),
1118
+ kimiApiFormat: settingsManager.getKimiApiFormat(),
1118
1119
  getToolContext: tc => toolContextStore.getContext(tc),
1119
1120
  getApiKey: async () => {
1120
1121
  const currentModel = agent.state.model;
package/src/tools/find.ts CHANGED
@@ -22,8 +22,7 @@ import { toolResult } from "./tool-result";
22
22
  import { type TruncationResult, truncateHead } from "./truncate";
23
23
 
24
24
  const findSchema = Type.Object({
25
- pattern: Type.String({ description: "Glob pattern, e.g. '*.ts', '**/*.json'" }),
26
- path: Type.Optional(Type.String({ description: "Directory to search (default: cwd)" })),
25
+ pattern: Type.String({ description: "Glob pattern, e.g. '*.ts', 'src/**/*.json', 'lib/*.tsx'" }),
27
26
  hidden: Type.Optional(Type.Boolean({ description: "Include hidden files and directories (default: true)" })),
28
27
  limit: Type.Optional(Type.Number({ description: "Max results (default: 1000)" })),
29
28
  });
@@ -31,6 +30,51 @@ const findSchema = Type.Object({
31
30
  const DEFAULT_LIMIT = 1000;
32
31
  const GLOB_TIMEOUT_MS = 5000;
33
32
 
33
+ /**
34
+ * Parse a pattern to extract the base directory path and glob pattern.
35
+ * Examples:
36
+ * "src/app/**\/*.tsx" → { basePath: "src/app", globPattern: "**\/*.tsx" }
37
+ * "src/app/*.tsx" → { basePath: "src/app", globPattern: "*.tsx" }
38
+ * "*.ts" → { basePath: ".", globPattern: "**\/*.ts" }
39
+ * "**\/*.json" → { basePath: ".", globPattern: "**\/*.json" }
40
+ * "/abs/path/**\/*.ts" → { basePath: "/abs/path", globPattern: "**\/*.ts" }
41
+ */
42
+ function parsePatternPath(pattern: string): { basePath: string; globPattern: string } {
43
+ // Find the first segment containing glob characters
44
+ const segments = pattern.split("/");
45
+ const globChars = ["*", "?", "[", "{"];
46
+
47
+ let firstGlobIndex = -1;
48
+ for (let i = 0; i < segments.length; i++) {
49
+ if (globChars.some(c => segments[i].includes(c))) {
50
+ firstGlobIndex = i;
51
+ break;
52
+ }
53
+ }
54
+
55
+ // No glob characters found - treat as literal path with implicit **/*
56
+ if (firstGlobIndex === -1) {
57
+ // Pattern is a directory path like "src/app" - search recursively in it
58
+ return { basePath: pattern, globPattern: "**/*" };
59
+ }
60
+
61
+ // Glob starts at first segment - no base path
62
+ if (firstGlobIndex === 0) {
63
+ // Simple pattern like "*.ts" needs **/ prefix for recursive search
64
+ const needsRecursive = !pattern.startsWith("**/");
65
+ return {
66
+ basePath: ".",
67
+ globPattern: needsRecursive ? `**/${pattern}` : pattern,
68
+ };
69
+ }
70
+
71
+ // Split at the glob boundary
72
+ const basePath = segments.slice(0, firstGlobIndex).join("/");
73
+ const globPattern = segments.slice(firstGlobIndex).join("/");
74
+
75
+ return { basePath, globPattern };
76
+ }
77
+
34
78
  export interface FindToolDetails {
35
79
  truncation?: TruncationResult;
36
80
  resultLimitReached?: number;
@@ -81,10 +125,19 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
81
125
  _onUpdate?: AgentToolUpdateCallback<FindToolDetails>,
82
126
  _context?: AgentToolContext,
83
127
  ): Promise<AgentToolResult<FindToolDetails>> {
84
- const { pattern, path: searchDir, limit, hidden } = params;
128
+ const { pattern, limit, hidden } = params;
85
129
 
86
130
  return untilAborted(signal, async () => {
87
- const searchPath = resolveToCwd(searchDir || ".", this.session.cwd);
131
+ // Parse pattern to extract base directory and glob pattern
132
+ // e.g., "src/app/**/*.tsx" → basePath: "src/app", globPattern: "**/*.tsx"
133
+ // e.g., "*.ts" → basePath: ".", globPattern: "**/*.ts"
134
+ const normalizedPattern = pattern.trim().replace(/\\/g, "/");
135
+ if (!normalizedPattern) {
136
+ throw new ToolError("Pattern must not be empty");
137
+ }
138
+
139
+ const { basePath, globPattern } = parsePatternPath(normalizedPattern);
140
+ const searchPath = resolveToCwd(basePath, this.session.cwd);
88
141
 
89
142
  if (searchPath === "/") {
90
143
  throw new ToolError("Searching from root directory '/' is not allowed");
@@ -94,10 +147,6 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
94
147
  const relative = path.relative(this.session.cwd, searchPath).replace(/\\/g, "/");
95
148
  return relative.length === 0 ? "." : relative;
96
149
  })();
97
- const normalizedPattern = pattern.trim();
98
- if (!normalizedPattern) {
99
- throw new ToolError("Pattern must not be empty");
100
- }
101
150
 
102
151
  const rawLimit = limit ?? DEFAULT_LIMIT;
103
152
  const effectiveLimit = Number.isFinite(rawLimit) ? Math.floor(rawLimit) : Number.NaN;
@@ -105,7 +154,6 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
105
154
  throw new ToolError("Limit must be a positive number");
106
155
  }
107
156
  const includeHidden = hidden ?? true;
108
- const globPattern = normalizedPattern.replace(/\\/g, "/");
109
157
 
110
158
  // If custom operations provided with glob, use that instead of fd
111
159
  if (this.customOps?.glob) {
@@ -113,7 +161,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
113
161
  throw new ToolError(`Path not found: ${searchPath}`);
114
162
  }
115
163
 
116
- const results = await this.customOps.glob(normalizedPattern, searchPath, {
164
+ const results = await this.customOps.glob(globPattern, searchPath, {
117
165
  ignore: ["**/node_modules/**", "**/.git/**"],
118
166
  limit: effectiveLimit,
119
167
  });
@@ -279,8 +327,6 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
279
327
 
280
328
  interface FindRenderArgs {
281
329
  pattern: string;
282
- path?: string;
283
- sortByMtime?: boolean;
284
330
  limit?: number;
285
331
  }
286
332
 
@@ -290,8 +336,6 @@ export const findToolRenderer = {
290
336
  inline: true,
291
337
  renderCall(args: FindRenderArgs, uiTheme: Theme): Component {
292
338
  const meta: string[] = [];
293
- if (args.path) meta.push(`in ${args.path}`);
294
- if (args.sortByMtime) meta.push("sort:mtime");
295
339
  if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
296
340
 
297
341
  const text = renderStatusLine(