@oh-my-pi/pi-coding-agent 1.337.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 +1228 -0
- package/README.md +1041 -0
- package/docs/compaction.md +403 -0
- package/docs/custom-tools.md +541 -0
- package/docs/extension-loading.md +1004 -0
- package/docs/hooks.md +867 -0
- package/docs/rpc.md +1040 -0
- package/docs/sdk.md +994 -0
- package/docs/session-tree-plan.md +441 -0
- package/docs/session.md +240 -0
- package/docs/skills.md +290 -0
- package/docs/theme.md +637 -0
- package/docs/tree.md +197 -0
- package/docs/tui.md +341 -0
- package/examples/README.md +21 -0
- package/examples/custom-tools/README.md +124 -0
- package/examples/custom-tools/hello/index.ts +20 -0
- package/examples/custom-tools/question/index.ts +84 -0
- package/examples/custom-tools/subagent/README.md +172 -0
- package/examples/custom-tools/subagent/agents/planner.md +37 -0
- package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
- package/examples/custom-tools/subagent/agents/scout.md +50 -0
- package/examples/custom-tools/subagent/agents/worker.md +24 -0
- package/examples/custom-tools/subagent/agents.ts +156 -0
- package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
- package/examples/custom-tools/subagent/commands/implement.md +10 -0
- package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
- package/examples/custom-tools/subagent/index.ts +1002 -0
- package/examples/custom-tools/todo/index.ts +212 -0
- package/examples/hooks/README.md +56 -0
- package/examples/hooks/auto-commit-on-exit.ts +49 -0
- package/examples/hooks/confirm-destructive.ts +59 -0
- package/examples/hooks/custom-compaction.ts +116 -0
- package/examples/hooks/dirty-repo-guard.ts +52 -0
- package/examples/hooks/file-trigger.ts +41 -0
- package/examples/hooks/git-checkpoint.ts +53 -0
- package/examples/hooks/handoff.ts +150 -0
- package/examples/hooks/permission-gate.ts +34 -0
- package/examples/hooks/protected-paths.ts +30 -0
- package/examples/hooks/qna.ts +119 -0
- package/examples/hooks/snake.ts +343 -0
- package/examples/hooks/status-line.ts +40 -0
- package/examples/sdk/01-minimal.ts +22 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +44 -0
- package/examples/sdk/04-skills.ts +44 -0
- package/examples/sdk/05-tools.ts +90 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +36 -0
- package/examples/sdk/08-slash-commands.ts +42 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
- package/examples/sdk/10-settings.ts +38 -0
- package/examples/sdk/11-sessions.ts +48 -0
- package/examples/sdk/12-full-control.ts +95 -0
- package/examples/sdk/README.md +154 -0
- package/package.json +81 -0
- package/src/cli/args.ts +246 -0
- package/src/cli/file-processor.ts +72 -0
- package/src/cli/list-models.ts +104 -0
- package/src/cli/plugin-cli.ts +650 -0
- package/src/cli/session-picker.ts +41 -0
- package/src/cli.ts +10 -0
- package/src/commands/init.md +20 -0
- package/src/config.ts +159 -0
- package/src/core/agent-session.ts +1900 -0
- package/src/core/auth-storage.ts +236 -0
- package/src/core/bash-executor.ts +196 -0
- package/src/core/compaction/branch-summarization.ts +343 -0
- package/src/core/compaction/compaction.ts +742 -0
- package/src/core/compaction/index.ts +7 -0
- package/src/core/compaction/utils.ts +154 -0
- package/src/core/custom-tools/index.ts +21 -0
- package/src/core/custom-tools/loader.ts +248 -0
- package/src/core/custom-tools/types.ts +169 -0
- package/src/core/custom-tools/wrapper.ts +28 -0
- package/src/core/exec.ts +129 -0
- package/src/core/export-html/index.ts +211 -0
- package/src/core/export-html/template.css +781 -0
- package/src/core/export-html/template.html +54 -0
- package/src/core/export-html/template.js +1185 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/loader.ts +312 -0
- package/src/core/hooks/runner.ts +434 -0
- package/src/core/hooks/tool-wrapper.ts +99 -0
- package/src/core/hooks/types.ts +773 -0
- package/src/core/index.ts +52 -0
- package/src/core/mcp/client.ts +158 -0
- package/src/core/mcp/config.ts +154 -0
- package/src/core/mcp/index.ts +45 -0
- package/src/core/mcp/loader.ts +68 -0
- package/src/core/mcp/manager.ts +181 -0
- package/src/core/mcp/tool-bridge.ts +148 -0
- package/src/core/mcp/transports/http.ts +316 -0
- package/src/core/mcp/transports/index.ts +6 -0
- package/src/core/mcp/transports/stdio.ts +252 -0
- package/src/core/mcp/types.ts +220 -0
- package/src/core/messages.ts +189 -0
- package/src/core/model-registry.ts +317 -0
- package/src/core/model-resolver.ts +393 -0
- package/src/core/plugins/doctor.ts +59 -0
- package/src/core/plugins/index.ts +38 -0
- package/src/core/plugins/installer.ts +189 -0
- package/src/core/plugins/loader.ts +338 -0
- package/src/core/plugins/manager.ts +672 -0
- package/src/core/plugins/parser.ts +105 -0
- package/src/core/plugins/paths.ts +32 -0
- package/src/core/plugins/types.ts +190 -0
- package/src/core/sdk.ts +760 -0
- package/src/core/session-manager.ts +1128 -0
- package/src/core/settings-manager.ts +443 -0
- package/src/core/skills.ts +437 -0
- package/src/core/slash-commands.ts +248 -0
- package/src/core/system-prompt.ts +439 -0
- package/src/core/timings.ts +25 -0
- package/src/core/tools/ask.ts +211 -0
- package/src/core/tools/bash-interceptor.ts +120 -0
- package/src/core/tools/bash.ts +250 -0
- package/src/core/tools/context.ts +32 -0
- package/src/core/tools/edit-diff.ts +475 -0
- package/src/core/tools/edit.ts +208 -0
- package/src/core/tools/exa/company.ts +59 -0
- package/src/core/tools/exa/index.ts +64 -0
- package/src/core/tools/exa/linkedin.ts +59 -0
- package/src/core/tools/exa/logger.ts +56 -0
- package/src/core/tools/exa/mcp-client.ts +368 -0
- package/src/core/tools/exa/render.ts +196 -0
- package/src/core/tools/exa/researcher.ts +90 -0
- package/src/core/tools/exa/search.ts +337 -0
- package/src/core/tools/exa/types.ts +168 -0
- package/src/core/tools/exa/websets.ts +248 -0
- package/src/core/tools/find.ts +261 -0
- package/src/core/tools/grep.ts +555 -0
- package/src/core/tools/index.ts +202 -0
- package/src/core/tools/ls.ts +140 -0
- package/src/core/tools/lsp/client.ts +605 -0
- package/src/core/tools/lsp/config.ts +147 -0
- package/src/core/tools/lsp/edits.ts +101 -0
- package/src/core/tools/lsp/index.ts +804 -0
- package/src/core/tools/lsp/render.ts +447 -0
- package/src/core/tools/lsp/rust-analyzer.ts +145 -0
- package/src/core/tools/lsp/types.ts +463 -0
- package/src/core/tools/lsp/utils.ts +486 -0
- package/src/core/tools/notebook.ts +229 -0
- package/src/core/tools/path-utils.ts +61 -0
- package/src/core/tools/read.ts +240 -0
- package/src/core/tools/renderers.ts +540 -0
- package/src/core/tools/task/agents.ts +153 -0
- package/src/core/tools/task/artifacts.ts +114 -0
- package/src/core/tools/task/bundled-agents/browser.md +71 -0
- package/src/core/tools/task/bundled-agents/explore.md +82 -0
- package/src/core/tools/task/bundled-agents/plan.md +54 -0
- package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
- package/src/core/tools/task/bundled-agents/task.md +53 -0
- package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
- package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
- package/src/core/tools/task/bundled-commands/implement.md +11 -0
- package/src/core/tools/task/commands.ts +213 -0
- package/src/core/tools/task/discovery.ts +208 -0
- package/src/core/tools/task/executor.ts +367 -0
- package/src/core/tools/task/index.ts +388 -0
- package/src/core/tools/task/model-resolver.ts +115 -0
- package/src/core/tools/task/parallel.ts +38 -0
- package/src/core/tools/task/render.ts +232 -0
- package/src/core/tools/task/types.ts +99 -0
- package/src/core/tools/truncate.ts +265 -0
- package/src/core/tools/web-fetch.ts +2370 -0
- package/src/core/tools/web-search/auth.ts +193 -0
- package/src/core/tools/web-search/index.ts +537 -0
- package/src/core/tools/web-search/providers/anthropic.ts +198 -0
- package/src/core/tools/web-search/providers/exa.ts +302 -0
- package/src/core/tools/web-search/providers/perplexity.ts +195 -0
- package/src/core/tools/web-search/render.ts +182 -0
- package/src/core/tools/web-search/types.ts +180 -0
- package/src/core/tools/write.ts +99 -0
- package/src/index.ts +176 -0
- package/src/main.ts +464 -0
- package/src/migrations.ts +135 -0
- package/src/modes/index.ts +43 -0
- package/src/modes/interactive/components/armin.ts +382 -0
- package/src/modes/interactive/components/assistant-message.ts +86 -0
- package/src/modes/interactive/components/bash-execution.ts +196 -0
- package/src/modes/interactive/components/bordered-loader.ts +41 -0
- package/src/modes/interactive/components/branch-summary-message.ts +42 -0
- package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
- package/src/modes/interactive/components/custom-editor.ts +122 -0
- package/src/modes/interactive/components/diff.ts +147 -0
- package/src/modes/interactive/components/dynamic-border.ts +25 -0
- package/src/modes/interactive/components/footer.ts +381 -0
- package/src/modes/interactive/components/hook-editor.ts +117 -0
- package/src/modes/interactive/components/hook-input.ts +64 -0
- package/src/modes/interactive/components/hook-message.ts +96 -0
- package/src/modes/interactive/components/hook-selector.ts +91 -0
- package/src/modes/interactive/components/model-selector.ts +247 -0
- package/src/modes/interactive/components/oauth-selector.ts +120 -0
- package/src/modes/interactive/components/plugin-settings.ts +479 -0
- package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
- package/src/modes/interactive/components/session-selector.ts +204 -0
- package/src/modes/interactive/components/settings-selector.ts +453 -0
- package/src/modes/interactive/components/show-images-selector.ts +45 -0
- package/src/modes/interactive/components/theme-selector.ts +62 -0
- package/src/modes/interactive/components/thinking-selector.ts +64 -0
- package/src/modes/interactive/components/tool-execution.ts +675 -0
- package/src/modes/interactive/components/tree-selector.ts +866 -0
- package/src/modes/interactive/components/user-message-selector.ts +159 -0
- package/src/modes/interactive/components/user-message.ts +18 -0
- package/src/modes/interactive/components/visual-truncate.ts +50 -0
- package/src/modes/interactive/components/welcome.ts +183 -0
- package/src/modes/interactive/interactive-mode.ts +2516 -0
- package/src/modes/interactive/theme/dark.json +101 -0
- package/src/modes/interactive/theme/light.json +98 -0
- package/src/modes/interactive/theme/theme-schema.json +308 -0
- package/src/modes/interactive/theme/theme.ts +998 -0
- package/src/modes/print-mode.ts +128 -0
- package/src/modes/rpc/rpc-client.ts +527 -0
- package/src/modes/rpc/rpc-mode.ts +483 -0
- package/src/modes/rpc/rpc-types.ts +203 -0
- package/src/utils/changelog.ts +99 -0
- package/src/utils/clipboard.ts +265 -0
- package/src/utils/fuzzy.ts +108 -0
- package/src/utils/mime.ts +30 -0
- package/src/utils/shell.ts +276 -0
- package/src/utils/tools-manager.ts +274 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
import { readFileSync, type Stats, statSync } from "node:fs";
|
|
2
|
+
import nodePath from "node:path";
|
|
3
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
import type { Subprocess } from "bun";
|
|
6
|
+
import { ensureTool } from "../../utils/tools-manager.js";
|
|
7
|
+
import { resolveToCwd } from "./path-utils.js";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_MAX_BYTES,
|
|
10
|
+
formatSize,
|
|
11
|
+
GREP_MAX_LINE_LENGTH,
|
|
12
|
+
type TruncationResult,
|
|
13
|
+
truncateHead,
|
|
14
|
+
truncateLine,
|
|
15
|
+
} from "./truncate.js";
|
|
16
|
+
|
|
17
|
+
const grepSchema = Type.Object({
|
|
18
|
+
pattern: Type.String({ description: "Search pattern (regex or literal string)" }),
|
|
19
|
+
path: Type.Optional(Type.String({ description: "Directory or file to search (default: current directory)" })),
|
|
20
|
+
glob: Type.Optional(Type.String({ description: "Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'" })),
|
|
21
|
+
type: Type.Optional(Type.String({ description: "File type filter (e.g., 'ts', 'rust', 'py')" })),
|
|
22
|
+
ignoreCase: Type.Optional(
|
|
23
|
+
Type.Boolean({ description: "Force case-insensitive search (default: false, uses smart-case otherwise)" }),
|
|
24
|
+
),
|
|
25
|
+
caseSensitive: Type.Optional(
|
|
26
|
+
Type.Boolean({ description: "Force case-sensitive search (default: false, disables smart-case)" }),
|
|
27
|
+
),
|
|
28
|
+
literal: Type.Optional(
|
|
29
|
+
Type.Boolean({ description: "Treat pattern as literal string instead of regex (default: false)" }),
|
|
30
|
+
),
|
|
31
|
+
multiline: Type.Optional(
|
|
32
|
+
Type.Boolean({ description: "Enable multiline matching for cross-line patterns (default: false)" }),
|
|
33
|
+
),
|
|
34
|
+
context: Type.Optional(
|
|
35
|
+
Type.Number({ description: "Number of lines to show before and after each match (default: 0)" }),
|
|
36
|
+
),
|
|
37
|
+
limit: Type.Optional(Type.Number({ description: "Maximum number of matches to return (default: 100)" })),
|
|
38
|
+
outputMode: Type.Optional(
|
|
39
|
+
Type.Union([Type.Literal("content"), Type.Literal("files_with_matches"), Type.Literal("count")], {
|
|
40
|
+
description:
|
|
41
|
+
"Output mode: 'content' shows matching lines, 'files_with_matches' shows only file paths, 'count' shows match counts per file (default: 'content')",
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
44
|
+
headLimit: Type.Optional(Type.Number({ description: "Limit output to first N results (default: unlimited)" })),
|
|
45
|
+
offset: Type.Optional(Type.Number({ description: "Skip first N results before applying headLimit (default: 0)" })),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const DEFAULT_LIMIT = 100;
|
|
49
|
+
|
|
50
|
+
export interface GrepToolDetails {
|
|
51
|
+
truncation?: TruncationResult;
|
|
52
|
+
matchLimitReached?: number;
|
|
53
|
+
linesTruncated?: boolean;
|
|
54
|
+
// Fields for TUI rendering
|
|
55
|
+
matchCount?: number;
|
|
56
|
+
fileCount?: number;
|
|
57
|
+
files?: string[];
|
|
58
|
+
mode?: "content" | "files_with_matches" | "count";
|
|
59
|
+
truncated?: boolean;
|
|
60
|
+
error?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createGrepTool(cwd: string): AgentTool<typeof grepSchema> {
|
|
64
|
+
return {
|
|
65
|
+
name: "grep",
|
|
66
|
+
label: "Grep",
|
|
67
|
+
description: `A powerful search tool built on ripgrep
|
|
68
|
+
|
|
69
|
+
Usage:
|
|
70
|
+
- ALWAYS use grep for search tasks. NEVER invoke \`grep\` or \`rg\` as a bash command. The grep tool has been optimized for correct permissions and access.
|
|
71
|
+
- Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")
|
|
72
|
+
- Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py", "rust")
|
|
73
|
+
- Output modes: "content" shows matching lines, "files_with_matches" shows only file paths (default), "count" shows match counts
|
|
74
|
+
- Use task tool for open-ended searches requiring multiple rounds
|
|
75
|
+
- Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping (use \`interface\\{\\}\` to find \`interface{}\` in Go code)
|
|
76
|
+
- Multiline matching: By default patterns match within single lines only. For cross-line patterns like \`struct \\{[\\s\\S]*?field\`, use \`multiline: true\``,
|
|
77
|
+
parameters: grepSchema,
|
|
78
|
+
execute: async (
|
|
79
|
+
_toolCallId: string,
|
|
80
|
+
{
|
|
81
|
+
pattern,
|
|
82
|
+
path: searchDir,
|
|
83
|
+
glob,
|
|
84
|
+
type,
|
|
85
|
+
ignoreCase,
|
|
86
|
+
caseSensitive,
|
|
87
|
+
literal,
|
|
88
|
+
multiline,
|
|
89
|
+
context,
|
|
90
|
+
limit,
|
|
91
|
+
outputMode,
|
|
92
|
+
headLimit,
|
|
93
|
+
offset,
|
|
94
|
+
}: {
|
|
95
|
+
pattern: string;
|
|
96
|
+
path?: string;
|
|
97
|
+
glob?: string;
|
|
98
|
+
type?: string;
|
|
99
|
+
ignoreCase?: boolean;
|
|
100
|
+
caseSensitive?: boolean;
|
|
101
|
+
literal?: boolean;
|
|
102
|
+
multiline?: boolean;
|
|
103
|
+
context?: number;
|
|
104
|
+
limit?: number;
|
|
105
|
+
outputMode?: "content" | "files_with_matches" | "count";
|
|
106
|
+
headLimit?: number;
|
|
107
|
+
offset?: number;
|
|
108
|
+
},
|
|
109
|
+
signal?: AbortSignal,
|
|
110
|
+
) => {
|
|
111
|
+
if (signal?.aborted) {
|
|
112
|
+
throw new Error("Operation aborted");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const rgPath = await ensureTool("rg", true);
|
|
116
|
+
if (!rgPath) {
|
|
117
|
+
throw new Error("ripgrep (rg) is not available and could not be downloaded");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const searchPath = resolveToCwd(searchDir || ".", cwd);
|
|
121
|
+
let searchStat: Stats;
|
|
122
|
+
try {
|
|
123
|
+
searchStat = statSync(searchPath);
|
|
124
|
+
} catch (_err) {
|
|
125
|
+
throw new Error(`Path not found: ${searchPath}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const isDirectory = searchStat.isDirectory();
|
|
129
|
+
const contextValue = context && context > 0 ? context : 0;
|
|
130
|
+
const effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT);
|
|
131
|
+
const effectiveOutputMode = outputMode ?? "content";
|
|
132
|
+
const effectiveOffset = offset && offset > 0 ? offset : 0;
|
|
133
|
+
const hasHeadLimit = headLimit !== undefined && headLimit > 0;
|
|
134
|
+
|
|
135
|
+
const formatPath = (filePath: string): string => {
|
|
136
|
+
if (isDirectory) {
|
|
137
|
+
const relative = nodePath.relative(searchPath, filePath);
|
|
138
|
+
if (relative && !relative.startsWith("..")) {
|
|
139
|
+
return relative.replace(/\\/g, "/");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return nodePath.basename(filePath);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const fileCache = new Map<string, string[]>();
|
|
146
|
+
const getFileLines = (filePath: string): string[] => {
|
|
147
|
+
let lines = fileCache.get(filePath);
|
|
148
|
+
if (!lines) {
|
|
149
|
+
try {
|
|
150
|
+
const content = readFileSync(filePath, "utf-8");
|
|
151
|
+
lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
152
|
+
} catch {
|
|
153
|
+
lines = [];
|
|
154
|
+
}
|
|
155
|
+
fileCache.set(filePath, lines);
|
|
156
|
+
}
|
|
157
|
+
return lines;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const args: string[] = [];
|
|
161
|
+
|
|
162
|
+
// Base arguments depend on output mode
|
|
163
|
+
if (effectiveOutputMode === "files_with_matches") {
|
|
164
|
+
args.push("--files-with-matches", "--color=never", "--hidden");
|
|
165
|
+
} else if (effectiveOutputMode === "count") {
|
|
166
|
+
args.push("--count", "--color=never", "--hidden");
|
|
167
|
+
} else {
|
|
168
|
+
args.push("--json", "--line-number", "--color=never", "--hidden");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (caseSensitive) {
|
|
172
|
+
args.push("--case-sensitive");
|
|
173
|
+
} else if (ignoreCase) {
|
|
174
|
+
args.push("--ignore-case");
|
|
175
|
+
} else {
|
|
176
|
+
args.push("--smart-case");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (multiline) {
|
|
180
|
+
args.push("--multiline");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (literal) {
|
|
184
|
+
args.push("--fixed-strings");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (glob) {
|
|
188
|
+
args.push("--glob", glob);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (type) {
|
|
192
|
+
args.push("--type", type);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
args.push(pattern, searchPath);
|
|
196
|
+
|
|
197
|
+
const child: Subprocess = Bun.spawn([rgPath, ...args], {
|
|
198
|
+
stdin: "ignore",
|
|
199
|
+
stdout: "pipe",
|
|
200
|
+
stderr: "pipe",
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
let stderr = "";
|
|
204
|
+
let matchCount = 0;
|
|
205
|
+
let matchLimitReached = false;
|
|
206
|
+
let linesTruncated = false;
|
|
207
|
+
let aborted = false;
|
|
208
|
+
let killedDueToLimit = false;
|
|
209
|
+
const outputLines: string[] = [];
|
|
210
|
+
const files = new Set<string>();
|
|
211
|
+
const fileList: string[] = [];
|
|
212
|
+
|
|
213
|
+
const recordFile = (filePath: string) => {
|
|
214
|
+
const relative = formatPath(filePath);
|
|
215
|
+
if (!files.has(relative)) {
|
|
216
|
+
files.add(relative);
|
|
217
|
+
fileList.push(relative);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const stopChild = (dueToLimit: boolean = false) => {
|
|
222
|
+
killedDueToLimit = dueToLimit;
|
|
223
|
+
child.kill();
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const onAbort = () => {
|
|
227
|
+
aborted = true;
|
|
228
|
+
stopChild();
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
if (signal) {
|
|
232
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// For simple output modes (files_with_matches, count), process text directly
|
|
236
|
+
if (effectiveOutputMode === "files_with_matches" || effectiveOutputMode === "count") {
|
|
237
|
+
const stdoutReader = (child.stdout as ReadableStream<Uint8Array>).getReader();
|
|
238
|
+
const stderrReader = (child.stderr as ReadableStream<Uint8Array>).getReader();
|
|
239
|
+
const decoder = new TextDecoder();
|
|
240
|
+
let stdout = "";
|
|
241
|
+
|
|
242
|
+
await Promise.all([
|
|
243
|
+
(async () => {
|
|
244
|
+
while (true) {
|
|
245
|
+
const { done, value } = await stdoutReader.read();
|
|
246
|
+
if (done) break;
|
|
247
|
+
stdout += decoder.decode(value, { stream: true });
|
|
248
|
+
}
|
|
249
|
+
})(),
|
|
250
|
+
(async () => {
|
|
251
|
+
while (true) {
|
|
252
|
+
const { done, value } = await stderrReader.read();
|
|
253
|
+
if (done) break;
|
|
254
|
+
stderr += decoder.decode(value, { stream: true });
|
|
255
|
+
}
|
|
256
|
+
})(),
|
|
257
|
+
]);
|
|
258
|
+
|
|
259
|
+
const exitCode = await child.exited;
|
|
260
|
+
|
|
261
|
+
if (signal) {
|
|
262
|
+
signal.removeEventListener("abort", onAbort);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (aborted) {
|
|
266
|
+
throw new Error("Operation aborted");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (exitCode !== 0 && exitCode !== 1) {
|
|
270
|
+
const errorMsg = stderr.trim() || `ripgrep exited with code ${exitCode}`;
|
|
271
|
+
throw new Error(errorMsg);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const lines = stdout
|
|
275
|
+
.trim()
|
|
276
|
+
.split("\n")
|
|
277
|
+
.filter((line) => line.length > 0);
|
|
278
|
+
|
|
279
|
+
if (lines.length === 0) {
|
|
280
|
+
return {
|
|
281
|
+
content: [{ type: "text", text: "No matches found" }],
|
|
282
|
+
details: {
|
|
283
|
+
matchCount: 0,
|
|
284
|
+
fileCount: 0,
|
|
285
|
+
files: [],
|
|
286
|
+
mode: effectiveOutputMode,
|
|
287
|
+
truncated: false,
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Apply offset and headLimit
|
|
293
|
+
let processedLines = lines;
|
|
294
|
+
if (effectiveOffset > 0) {
|
|
295
|
+
processedLines = processedLines.slice(effectiveOffset);
|
|
296
|
+
}
|
|
297
|
+
if (hasHeadLimit) {
|
|
298
|
+
processedLines = processedLines.slice(0, headLimit);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let simpleMatchCount = 0;
|
|
302
|
+
let fileCount = 0;
|
|
303
|
+
const simpleFiles = new Set<string>();
|
|
304
|
+
const simpleFileList: string[] = [];
|
|
305
|
+
|
|
306
|
+
const recordSimpleFile = (filePath: string) => {
|
|
307
|
+
const relative = formatPath(filePath);
|
|
308
|
+
if (!simpleFiles.has(relative)) {
|
|
309
|
+
simpleFiles.add(relative);
|
|
310
|
+
simpleFileList.push(relative);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
if (effectiveOutputMode === "files_with_matches") {
|
|
315
|
+
for (const line of lines) {
|
|
316
|
+
recordSimpleFile(line);
|
|
317
|
+
}
|
|
318
|
+
fileCount = simpleFiles.size;
|
|
319
|
+
simpleMatchCount = fileCount;
|
|
320
|
+
} else {
|
|
321
|
+
for (const line of lines) {
|
|
322
|
+
const separatorIndex = line.lastIndexOf(":");
|
|
323
|
+
const filePart = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
|
|
324
|
+
const countPart = separatorIndex === -1 ? "" : line.slice(separatorIndex + 1);
|
|
325
|
+
const count = Number.parseInt(countPart, 10);
|
|
326
|
+
recordSimpleFile(filePart);
|
|
327
|
+
if (!Number.isNaN(count)) {
|
|
328
|
+
simpleMatchCount += count;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
fileCount = simpleFiles.size;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const truncated = hasHeadLimit && processedLines.length < lines.length;
|
|
335
|
+
|
|
336
|
+
// For count mode, format as "path:count"
|
|
337
|
+
if (effectiveOutputMode === "count") {
|
|
338
|
+
const formatted = processedLines.map((line) => {
|
|
339
|
+
const separatorIndex = line.lastIndexOf(":");
|
|
340
|
+
const relative = formatPath(separatorIndex === -1 ? line : line.slice(0, separatorIndex));
|
|
341
|
+
const count = separatorIndex === -1 ? "0" : line.slice(separatorIndex + 1);
|
|
342
|
+
return `${relative}:${count}`;
|
|
343
|
+
});
|
|
344
|
+
const output = formatted.join("\n");
|
|
345
|
+
return {
|
|
346
|
+
content: [{ type: "text", text: output }],
|
|
347
|
+
details: {
|
|
348
|
+
matchCount: simpleMatchCount,
|
|
349
|
+
fileCount,
|
|
350
|
+
files: simpleFileList.slice(0, 50),
|
|
351
|
+
mode: effectiveOutputMode,
|
|
352
|
+
truncated,
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// For files_with_matches, format paths
|
|
358
|
+
const formatted = processedLines.map((line) => formatPath(line));
|
|
359
|
+
const output = formatted.join("\n");
|
|
360
|
+
return {
|
|
361
|
+
content: [{ type: "text", text: output }],
|
|
362
|
+
details: {
|
|
363
|
+
matchCount: simpleMatchCount,
|
|
364
|
+
fileCount,
|
|
365
|
+
files: simpleFileList.slice(0, 50),
|
|
366
|
+
mode: effectiveOutputMode,
|
|
367
|
+
truncated,
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Content mode - existing JSON processing
|
|
373
|
+
const formatBlock = (filePath: string, lineNumber: number): string[] => {
|
|
374
|
+
const relativePath = formatPath(filePath);
|
|
375
|
+
const lines = getFileLines(filePath);
|
|
376
|
+
if (!lines.length) {
|
|
377
|
+
return [`${relativePath}:${lineNumber}: (unable to read file)`];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const block: string[] = [];
|
|
381
|
+
const start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;
|
|
382
|
+
const end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;
|
|
383
|
+
|
|
384
|
+
for (let current = start; current <= end; current++) {
|
|
385
|
+
const lineText = lines[current - 1] ?? "";
|
|
386
|
+
const sanitized = lineText.replace(/\r/g, "");
|
|
387
|
+
const isMatchLine = current === lineNumber;
|
|
388
|
+
|
|
389
|
+
const { text: truncatedText, wasTruncated } = truncateLine(sanitized);
|
|
390
|
+
if (wasTruncated) {
|
|
391
|
+
linesTruncated = true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (isMatchLine) {
|
|
395
|
+
block.push(`${relativePath}:${current}: ${truncatedText}`);
|
|
396
|
+
} else {
|
|
397
|
+
block.push(`${relativePath}-${current}- ${truncatedText}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return block;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const processLine = (line: string) => {
|
|
405
|
+
if (!line.trim() || matchCount >= effectiveLimit) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let event: { type: string; data?: { path?: { text?: string }; line_number?: number } };
|
|
410
|
+
try {
|
|
411
|
+
event = JSON.parse(line);
|
|
412
|
+
} catch {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (event.type === "match") {
|
|
417
|
+
matchCount++;
|
|
418
|
+
const filePath = event.data?.path?.text;
|
|
419
|
+
const lineNumber = event.data?.line_number;
|
|
420
|
+
|
|
421
|
+
if (filePath && typeof lineNumber === "number") {
|
|
422
|
+
recordFile(filePath);
|
|
423
|
+
outputLines.push(...formatBlock(filePath, lineNumber));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (matchCount >= effectiveLimit) {
|
|
427
|
+
matchLimitReached = true;
|
|
428
|
+
stopChild(true);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// Read streams using Bun's ReadableStream API
|
|
434
|
+
const stdoutReader = (child.stdout as ReadableStream<Uint8Array>).getReader();
|
|
435
|
+
const stderrReader = (child.stderr as ReadableStream<Uint8Array>).getReader();
|
|
436
|
+
const decoder = new TextDecoder();
|
|
437
|
+
let stdoutBuffer = "";
|
|
438
|
+
|
|
439
|
+
await Promise.all([
|
|
440
|
+
// Process stdout line by line
|
|
441
|
+
(async () => {
|
|
442
|
+
while (true) {
|
|
443
|
+
const { done, value } = await stdoutReader.read();
|
|
444
|
+
if (done) break;
|
|
445
|
+
|
|
446
|
+
stdoutBuffer += decoder.decode(value, { stream: true });
|
|
447
|
+
const lines = stdoutBuffer.split("\n");
|
|
448
|
+
// Keep the last incomplete line in the buffer
|
|
449
|
+
stdoutBuffer = lines.pop() ?? "";
|
|
450
|
+
|
|
451
|
+
for (const line of lines) {
|
|
452
|
+
processLine(line);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// Process any remaining content
|
|
456
|
+
if (stdoutBuffer.trim()) {
|
|
457
|
+
processLine(stdoutBuffer);
|
|
458
|
+
}
|
|
459
|
+
})(),
|
|
460
|
+
// Collect stderr
|
|
461
|
+
(async () => {
|
|
462
|
+
while (true) {
|
|
463
|
+
const { done, value } = await stderrReader.read();
|
|
464
|
+
if (done) break;
|
|
465
|
+
stderr += decoder.decode(value, { stream: true });
|
|
466
|
+
}
|
|
467
|
+
})(),
|
|
468
|
+
]);
|
|
469
|
+
|
|
470
|
+
const exitCode = await child.exited;
|
|
471
|
+
|
|
472
|
+
// Cleanup
|
|
473
|
+
if (signal) {
|
|
474
|
+
signal.removeEventListener("abort", onAbort);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (aborted) {
|
|
478
|
+
throw new Error("Operation aborted");
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (!killedDueToLimit && exitCode !== 0 && exitCode !== 1) {
|
|
482
|
+
const errorMsg = stderr.trim() || `ripgrep exited with code ${exitCode}`;
|
|
483
|
+
throw new Error(errorMsg);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (matchCount === 0) {
|
|
487
|
+
return {
|
|
488
|
+
content: [{ type: "text", text: "No matches found" }],
|
|
489
|
+
details: {
|
|
490
|
+
matchCount: 0,
|
|
491
|
+
fileCount: 0,
|
|
492
|
+
files: [],
|
|
493
|
+
mode: effectiveOutputMode,
|
|
494
|
+
truncated: false,
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Apply offset and headLimit to output lines
|
|
500
|
+
let processedLines = outputLines;
|
|
501
|
+
if (effectiveOffset > 0) {
|
|
502
|
+
processedLines = processedLines.slice(effectiveOffset);
|
|
503
|
+
}
|
|
504
|
+
if (hasHeadLimit) {
|
|
505
|
+
processedLines = processedLines.slice(0, headLimit);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Apply byte truncation (no line limit since we already have match limit)
|
|
509
|
+
const rawOutput = processedLines.join("\n");
|
|
510
|
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
511
|
+
|
|
512
|
+
let output = truncation.content;
|
|
513
|
+
const truncatedByHeadLimit = hasHeadLimit && processedLines.length < outputLines.length;
|
|
514
|
+
const details: GrepToolDetails = {
|
|
515
|
+
matchCount,
|
|
516
|
+
fileCount: files.size,
|
|
517
|
+
files: fileList.slice(0, 50),
|
|
518
|
+
mode: effectiveOutputMode,
|
|
519
|
+
truncated: matchLimitReached || truncation.truncated || truncatedByHeadLimit,
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
// Build notices
|
|
523
|
+
const notices: string[] = [];
|
|
524
|
+
|
|
525
|
+
if (matchLimitReached) {
|
|
526
|
+
notices.push(
|
|
527
|
+
`${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,
|
|
528
|
+
);
|
|
529
|
+
details.matchLimitReached = effectiveLimit;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (truncation.truncated) {
|
|
533
|
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
534
|
+
details.truncation = truncation;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (linesTruncated) {
|
|
538
|
+
notices.push(`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`);
|
|
539
|
+
details.linesTruncated = true;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (notices.length > 0) {
|
|
543
|
+
output += `\n\n[${notices.join(". ")}]`;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
content: [{ type: "text", text: output }],
|
|
548
|
+
details: Object.keys(details).length > 0 ? details : undefined,
|
|
549
|
+
};
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Default grep tool using process.cwd() - for backwards compatibility */
|
|
555
|
+
export const grepTool = createGrepTool(process.cwd());
|