@trim21/personal-pi-extensions 0.0.225 → 0.0.230
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/README.md +18 -40
- package/package.json +3 -6
- package/src/bwrap/runtime.ts +16 -14
- package/src/claude-code/common.ts +7 -1
- package/src/claude-code/files.ts +17 -2
- package/src/claude-code/glob.ts +73 -0
- package/src/claude-code/grep.ts +161 -0
- package/src/claude-code/search.ts +16 -212
- package/src/claude-code/session-tools.ts +24 -14
- package/src/claude-code/shell.ts +1 -0
- package/src/lib/ui.ts +54 -0
- package/src/lib/write-guard.ts +153 -0
- package/src/opencode/bash.ts +5 -1
- package/src/opencode/edit-engine.ts +2 -2
- package/src/opencode/edit.ts +8 -1
- package/src/opencode/index.ts +1 -1
- package/src/opencode/question.ts +12 -8
- package/src/opencode/write.ts +6 -0
- package/src/spawn-agent.ts +14 -14
- package/src/bash-default-timeout.ts +0 -10
- package/src/bwrap/index.ts +0 -15
- package/src/workspace-guard.ts +0 -253
package/README.md
CHANGED
|
@@ -6,17 +6,16 @@
|
|
|
6
6
|
|
|
7
7
|
## 扩展概览
|
|
8
8
|
|
|
9
|
-
| 扩展
|
|
10
|
-
|
|
|
11
|
-
| [bwrap](#bwrap)
|
|
12
|
-
| [
|
|
13
|
-
| [opencode-edit](#opencode-edit)
|
|
14
|
-
| [
|
|
15
|
-
| [
|
|
16
|
-
| [
|
|
17
|
-
| [
|
|
18
|
-
| [
|
|
19
|
-
| [talk](#talk) | session 间消息传递,SQLite 邮箱 + 双向 ask 时间戳仲裁 |
|
|
9
|
+
| 扩展 | 描述 |
|
|
10
|
+
| ------------------------------- | ------------------------------------------------------- |
|
|
11
|
+
| [bwrap](#bwrap) | 基于 bubblewrap 的 OS 级沙箱,提供文件系统和网络隔离 |
|
|
12
|
+
| [写保护(内置)](#写保护内置) | 写工具内置:限制文件写入在 workspace 内,外部写入需审批 |
|
|
13
|
+
| [opencode-edit](#opencode-edit) | 替换内置 edit 工具,使用 opencode 的 schema 和匹配引擎 |
|
|
14
|
+
| [vision-agent](#vision-agent) | 视觉代理:主模型不支持视觉时,spawn 子 agent 识别图片 |
|
|
15
|
+
| [session-name](#session-name) | 首个 user prompt 自动生成会话名,模型命名 + 启发式兜底 |
|
|
16
|
+
| [todowrite](#todowrite) | opencode 风格的任务列表工具,完整列表替换语义 |
|
|
17
|
+
| [question](#question) | opencode 风格的提问工具,阻塞式询问用户选择 |
|
|
18
|
+
| [talk](#talk) | session 间消息传递,SQLite 邮箱 + 双向 ask 时间戳仲裁 |
|
|
20
19
|
|
|
21
20
|
---
|
|
22
21
|
|
|
@@ -79,32 +78,24 @@ bash 工具(opencode 风格 `bash`、Claude Code 风格 `Bash`)注册了 `da
|
|
|
79
78
|
|
|
80
79
|
### 使用
|
|
81
80
|
|
|
82
|
-
|
|
83
|
-
pi -e ./src/bwrap/index.ts
|
|
84
|
-
# 或通过配置文件注册后自动加载
|
|
85
|
-
```
|
|
81
|
+
bwrap 已集成进 bash 工具实现(opencode 风格 `bash` 位于 `src/opencode/bash.ts`,Claude Code 风格 `Bash` 位于 `src/claude-code/shell.ts`),随对应扩展一起加载,无需单独安装。bash 工具内置默认超时 120 秒。
|
|
86
82
|
|
|
87
83
|
---
|
|
88
84
|
|
|
89
|
-
##
|
|
85
|
+
## 写保护(内置)
|
|
90
86
|
|
|
91
|
-
|
|
87
|
+
写保护直接内置在各写工具(opencode 风格 `write`/`edit`、Claude Code 风格 `Write`/`Edit`)内部,通过 `src/lib/write-guard.ts` 的 `guardWriteAccess` 实现。读取工具(`read`、`ls`、`find`、`grep`)不受限制。
|
|
92
88
|
|
|
93
89
|
- workspace 内或 `/tmp` 下的路径自动放行
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
|
|
97
|
-
### 使用
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
pi -e ./src/workspace-guard.ts
|
|
101
|
-
```
|
|
90
|
+
- 外部路径需通过确认对话框由用户审批,对话框内以 ```diff 代码块展示将要发生的变更预览(与 opencode-edit 共享匹配引擎,能定位时显示带行号的真实 patch,否则退化为参数 diff)
|
|
91
|
+
- headless(无 UI)会话直接拒绝外部写入
|
|
92
|
+
- 无需配置,随各写工具自动生效
|
|
102
93
|
|
|
103
94
|
---
|
|
104
95
|
|
|
105
96
|
## opencode-edit
|
|
106
97
|
|
|
107
|
-
替换内置 `edit` 工具,使用 [opencode](https://github.com/anomalyco/opencode) 的 schema 和模糊匹配引擎。核心 replacer 和 `replace()` 函数直接复制自 opencode,行为与原版完全一致。匹配引擎位于 `src/opencode
|
|
98
|
+
替换内置 `edit` 工具,使用 [opencode](https://github.com/anomalyco/opencode) 的 schema 和模糊匹配引擎。核心 replacer 和 `replace()` 函数直接复制自 opencode,行为与原版完全一致。匹配引擎位于 `src/opencode/edit-engine.ts`,与写保护审批弹窗(`src/lib/write-guard.ts`)的 diff 预览共享。
|
|
108
99
|
|
|
109
100
|
支持的匹配策略:
|
|
110
101
|
|
|
@@ -128,18 +119,6 @@ pi -e ./src/opencode-edit.ts
|
|
|
128
119
|
|
|
129
120
|
---
|
|
130
121
|
|
|
131
|
-
## bash-default-timeout
|
|
132
|
-
|
|
133
|
-
为所有 bash 工具调用设置 180 秒默认超时。仅在模型未显式指定 `timeout` 时生效,避免长时间运行的命令无限挂起。
|
|
134
|
-
|
|
135
|
-
### 使用
|
|
136
|
-
|
|
137
|
-
```bash
|
|
138
|
-
pi -e ./src/bash-default-timeout.ts
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
---
|
|
142
|
-
|
|
143
122
|
## vision-agent
|
|
144
123
|
|
|
145
124
|
视觉代理扩展。主模型不支持视觉(如 DeepSeek)时自动启用 `describe_image` 工具;主模型支持视觉时自动隐藏,图片由 pi 原生透传。
|
|
@@ -362,8 +341,7 @@ pi -e ./src/talk/index.ts
|
|
|
362
341
|
### 命令行加载单个扩展
|
|
363
342
|
|
|
364
343
|
```bash
|
|
365
|
-
pi -e ./src/
|
|
366
|
-
pi -e ./src/workspace-guard.ts
|
|
344
|
+
pi -e ./src/opencode/index.ts
|
|
367
345
|
```
|
|
368
346
|
|
|
369
347
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trim21/personal-pi-extensions",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.230",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
|
|
6
6
|
"keywords": [
|
|
@@ -54,13 +54,10 @@
|
|
|
54
54
|
},
|
|
55
55
|
"pi": {
|
|
56
56
|
"extensions": [
|
|
57
|
+
"src/opencode/index.ts",
|
|
58
|
+
"src/claude-code/index.ts",
|
|
57
59
|
"src/vision-agent.ts",
|
|
58
60
|
"src/session-name.ts",
|
|
59
|
-
"src/bwrap/index.ts",
|
|
60
|
-
"src/workspace-guard.ts",
|
|
61
|
-
"src/claude-code/index.ts",
|
|
62
|
-
"src/opencode/index.ts",
|
|
63
|
-
"src/bash-default-timeout.ts",
|
|
64
61
|
"src/gh-readonly.ts",
|
|
65
62
|
"src/spawn-agent.ts",
|
|
66
63
|
"src/talk/index.ts"
|
package/src/bwrap/runtime.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { createBashTool } from "@earendil-works/pi-coding-agent";
|
|
|
7
7
|
import { type TObject, Type } from "typebox";
|
|
8
8
|
|
|
9
9
|
import { type CommandSpec, parseCommand } from "../lib/cli.js";
|
|
10
|
+
import { selectWithOptionalInput } from "../lib/ui.js";
|
|
10
11
|
import {
|
|
11
12
|
type BwrapMode,
|
|
12
13
|
createBwrapBashOperations,
|
|
@@ -163,26 +164,27 @@ export class BwrapRuntime {
|
|
|
163
164
|
if (policy.kind === "deny") throw new Error(policy.reason);
|
|
164
165
|
const description = `Allow this command to run without sandbox?\n---\n\nReason: ${escapeHtml(reason ?? "(No reason provided by model)")}\n---\n${fenceCodeBlock(command)}`;
|
|
165
166
|
while (true) {
|
|
166
|
-
const
|
|
167
|
+
const result = await selectWithOptionalInput(
|
|
167
168
|
description,
|
|
168
|
-
[
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
[
|
|
170
|
+
{ label: "Approve once" },
|
|
171
|
+
{ label: "Block" },
|
|
172
|
+
{ label: "Block with reason", inputPrompt: "Why was this denied?" },
|
|
173
|
+
],
|
|
174
|
+
ctx.ui,
|
|
175
|
+
{ signal: ctx.signal },
|
|
172
176
|
);
|
|
173
|
-
if (
|
|
177
|
+
if (result === undefined) {
|
|
174
178
|
ctx.abort();
|
|
175
179
|
throw new Error("User denied the command execution.");
|
|
176
180
|
}
|
|
177
|
-
if (
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
});
|
|
182
|
-
if (feedback === undefined) continue;
|
|
181
|
+
if (result.label === "Approve once") return;
|
|
182
|
+
if (result.label === "Block") throw new Error("User denied unsandboxed execution.");
|
|
183
|
+
// "Block with reason":取消输入则重新询问;空输入同样拒绝(不带反馈文本)
|
|
184
|
+
if (result.input === undefined) continue;
|
|
183
185
|
throw new Error(
|
|
184
|
-
|
|
185
|
-
? `User denied unsandboxed execution: ${
|
|
186
|
+
result.input
|
|
187
|
+
? `User denied unsandboxed execution: ${result.input}`
|
|
186
188
|
: "User denied unsandboxed execution.",
|
|
187
189
|
);
|
|
188
190
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isAbsolute, normalize } from "node:path";
|
|
1
|
+
import { isAbsolute, normalize, resolve } from "node:path";
|
|
2
2
|
|
|
3
3
|
export interface FileSnapshot {
|
|
4
4
|
digest: string;
|
|
@@ -20,6 +20,12 @@ export function requireAbsolutePath(filePath: string, parameter = "file_path"):
|
|
|
20
20
|
return normalize(filePath);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** Resolve a search root path against the working directory. */
|
|
24
|
+
export function searchRoot(path: string | undefined, cwd: string): string {
|
|
25
|
+
if (!path) return cwd;
|
|
26
|
+
return isAbsolute(path) ? path : resolve(cwd, path);
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
export function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
24
30
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
25
31
|
}
|
package/src/claude-code/files.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { Type } from "typebox";
|
|
14
14
|
|
|
15
|
+
import { guardWriteAccess } from "../lib/write-guard.js";
|
|
15
16
|
import {
|
|
16
17
|
type ClaudeCodeState,
|
|
17
18
|
type FileSnapshot,
|
|
@@ -241,9 +242,18 @@ export function registerFileTools(pi: ExtensionAPI, state: ClaudeCodeState): voi
|
|
|
241
242
|
},
|
|
242
243
|
{ additionalProperties: false },
|
|
243
244
|
),
|
|
244
|
-
async execute(_id, params, signal) {
|
|
245
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
245
246
|
throwIfAborted(signal);
|
|
246
247
|
const filePath = requireAbsolutePath(params.file_path);
|
|
248
|
+
await guardWriteAccess(ctx, {
|
|
249
|
+
toolName: "Edit",
|
|
250
|
+
absolutePath: filePath,
|
|
251
|
+
change: {
|
|
252
|
+
oldText: params.old_string,
|
|
253
|
+
newText: params.new_string,
|
|
254
|
+
replaceAll: params.replace_all,
|
|
255
|
+
},
|
|
256
|
+
});
|
|
247
257
|
return withFileMutationQueue(filePath, async () => {
|
|
248
258
|
await requireCurrentRead(state, filePath);
|
|
249
259
|
await access(filePath, constants.R_OK | constants.W_OK);
|
|
@@ -287,9 +297,14 @@ export function registerFileTools(pi: ExtensionAPI, state: ClaudeCodeState): voi
|
|
|
287
297
|
},
|
|
288
298
|
{ additionalProperties: false },
|
|
289
299
|
),
|
|
290
|
-
async execute(_id, params, signal) {
|
|
300
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
291
301
|
throwIfAborted(signal);
|
|
292
302
|
const filePath = requireAbsolutePath(params.file_path);
|
|
303
|
+
await guardWriteAccess(ctx, {
|
|
304
|
+
toolName: "Write",
|
|
305
|
+
absolutePath: filePath,
|
|
306
|
+
change: { oldText: "", newText: params.content },
|
|
307
|
+
});
|
|
293
308
|
return withFileMutationQueue(filePath, async () => {
|
|
294
309
|
let original: string | undefined;
|
|
295
310
|
try {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code style `Glob` tool — fast file pattern matching.
|
|
3
|
+
*
|
|
4
|
+
* Split out of the former search.ts so spawn-agent subagents can load it
|
|
5
|
+
* independently (declare `Glob` in the frontmatter to get only this tool).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { glob as fsGlob, stat } from "node:fs/promises";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
|
|
11
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { Type } from "typebox";
|
|
13
|
+
|
|
14
|
+
import { searchRoot, throwIfAborted } from "./common.js";
|
|
15
|
+
|
|
16
|
+
const GLOB_RESULT_LIMIT = 100;
|
|
17
|
+
|
|
18
|
+
export async function globFiles(
|
|
19
|
+
pattern: string,
|
|
20
|
+
cwd: string,
|
|
21
|
+
signal?: AbortSignal,
|
|
22
|
+
): Promise<string[]> {
|
|
23
|
+
throwIfAborted(signal);
|
|
24
|
+
const matches: { path: string; mtimeMs: number }[] = [];
|
|
25
|
+
for await (const match of fsGlob(pattern, { cwd, exclude: [".git/**"], withFileTypes: false })) {
|
|
26
|
+
throwIfAborted(signal);
|
|
27
|
+
const absolutePath = resolve(cwd, match);
|
|
28
|
+
try {
|
|
29
|
+
const value = await stat(absolutePath);
|
|
30
|
+
if (value.isFile()) matches.push({ path: absolutePath, mtimeMs: value.mtimeMs });
|
|
31
|
+
} catch {
|
|
32
|
+
// A concurrent filesystem change can remove a match before stat.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return matches
|
|
36
|
+
.toSorted((left, right) => right.mtimeMs - left.mtimeMs || left.path.localeCompare(right.path))
|
|
37
|
+
.slice(0, GLOB_RESULT_LIMIT)
|
|
38
|
+
.map((match) => match.path);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function registerGlobTool(pi: ExtensionAPI): void {
|
|
42
|
+
pi.registerTool({
|
|
43
|
+
name: "Glob",
|
|
44
|
+
label: "Glob",
|
|
45
|
+
description: [
|
|
46
|
+
"Fast file pattern matching tool that works with any codebase size.",
|
|
47
|
+
'Supports glob patterns such as "**/*.js" and "src/**/*.ts".',
|
|
48
|
+
"Returns matching file paths sorted by modification time.",
|
|
49
|
+
].join("\n"),
|
|
50
|
+
parameters: Type.Object(
|
|
51
|
+
{
|
|
52
|
+
pattern: Type.String({ description: "The glob pattern to match files against" }),
|
|
53
|
+
path: Type.Optional(
|
|
54
|
+
Type.String({
|
|
55
|
+
description:
|
|
56
|
+
"The directory to search in. If omitted, the current working directory is used.",
|
|
57
|
+
}),
|
|
58
|
+
),
|
|
59
|
+
},
|
|
60
|
+
{ additionalProperties: false },
|
|
61
|
+
),
|
|
62
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
63
|
+
const root = searchRoot(params.path, ctx.cwd);
|
|
64
|
+
const matches = await globFiles(params.pattern, root, signal);
|
|
65
|
+
return {
|
|
66
|
+
content: [
|
|
67
|
+
{ type: "text", text: matches.length > 0 ? matches.join("\n") : "No files found" },
|
|
68
|
+
],
|
|
69
|
+
details: { count: matches.length },
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code style `Grep` tool — powerful ripgrep-based search.
|
|
3
|
+
*
|
|
4
|
+
* Split out of the former search.ts so spawn-agent subagents can load it
|
|
5
|
+
* independently (declare `Grep` in the frontmatter to get only this tool).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
9
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { Type } from "typebox";
|
|
11
|
+
|
|
12
|
+
import { searchRoot, throwIfAborted } from "./common.js";
|
|
13
|
+
|
|
14
|
+
const GREP_OUTPUT_MODES = ["content", "files_with_matches", "count"] as const;
|
|
15
|
+
|
|
16
|
+
type GrepOutputMode = (typeof GREP_OUTPUT_MODES)[number];
|
|
17
|
+
|
|
18
|
+
function truncateOutput(output: string, maxCharacters = 30_000): string {
|
|
19
|
+
if (output.length <= maxCharacters) return output;
|
|
20
|
+
return `${output.slice(0, maxCharacters)}\n\n[Output truncated at ${maxCharacters} characters]`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface GrepParameters {
|
|
24
|
+
pattern: string;
|
|
25
|
+
path?: string;
|
|
26
|
+
glob?: string;
|
|
27
|
+
output_mode?: GrepOutputMode;
|
|
28
|
+
"-B"?: number;
|
|
29
|
+
"-A"?: number;
|
|
30
|
+
"-C"?: number;
|
|
31
|
+
context?: number;
|
|
32
|
+
"-n"?: boolean;
|
|
33
|
+
"-i"?: boolean;
|
|
34
|
+
type?: string;
|
|
35
|
+
head_limit?: number;
|
|
36
|
+
offset?: number;
|
|
37
|
+
multiline?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildGrepArguments(params: GrepParameters, cwd: string): string[] {
|
|
41
|
+
const mode = params.output_mode ?? "files_with_matches";
|
|
42
|
+
const args = ["--color=never"];
|
|
43
|
+
switch (mode) {
|
|
44
|
+
case "files_with_matches": {
|
|
45
|
+
args.push("--files-with-matches");
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case "count": {
|
|
49
|
+
args.push("--count-matches");
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case "content": {
|
|
53
|
+
args.push("--no-heading", "--with-filename");
|
|
54
|
+
if (params["-n"] !== false) args.push("--line-number");
|
|
55
|
+
const before = params["-B"];
|
|
56
|
+
const after = params["-A"];
|
|
57
|
+
const around = params.context ?? params["-C"];
|
|
58
|
+
if (around === undefined) {
|
|
59
|
+
if (before !== undefined) args.push("--before-context", String(before));
|
|
60
|
+
if (after !== undefined) args.push("--after-context", String(after));
|
|
61
|
+
} else args.push("--context", String(around));
|
|
62
|
+
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
// No default
|
|
66
|
+
}
|
|
67
|
+
if (params["-i"] === true) args.push("--ignore-case");
|
|
68
|
+
if (params.glob) args.push("--glob", params.glob);
|
|
69
|
+
if (params.type) args.push("--type", params.type);
|
|
70
|
+
if (params.multiline === true) args.push("--multiline", "--multiline-dotall");
|
|
71
|
+
args.push("--", params.pattern, searchRoot(params.path, cwd));
|
|
72
|
+
return args;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function pageGrepOutput(output: string, offset = 0, headLimit = 0): string {
|
|
76
|
+
const lines = output ? output.replace(/\n$/, "").split("\n") : [];
|
|
77
|
+
if (offset >= lines.length && lines.length > 0) return "No entries at this offset";
|
|
78
|
+
const selected = headLimit > 0 ? lines.slice(offset, offset + headLimit) : lines.slice(offset);
|
|
79
|
+
return truncateOutput(selected.join("\n"));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function registerGrepTool(pi: ExtensionAPI): void {
|
|
83
|
+
pi.registerTool({
|
|
84
|
+
name: "Grep",
|
|
85
|
+
label: "Grep",
|
|
86
|
+
description: [
|
|
87
|
+
"A powerful search tool built on ripgrep.",
|
|
88
|
+
"Supports regular expressions, file globs, file types, multiline matching, context lines, and paginated output.",
|
|
89
|
+
'output_mode defaults to "files_with_matches"; use "content" for matching lines or "count" for match counts.',
|
|
90
|
+
].join("\n"),
|
|
91
|
+
parameters: Type.Object(
|
|
92
|
+
{
|
|
93
|
+
pattern: Type.String({ description: "The regular expression pattern to search for" }),
|
|
94
|
+
path: Type.Optional(
|
|
95
|
+
Type.String({
|
|
96
|
+
description: "File or directory to search. Defaults to the current directory.",
|
|
97
|
+
}),
|
|
98
|
+
),
|
|
99
|
+
glob: Type.Optional(
|
|
100
|
+
Type.String({ description: 'Glob filter such as "*.js" or "*.{ts,tsx}"' }),
|
|
101
|
+
),
|
|
102
|
+
output_mode: Type.Optional(
|
|
103
|
+
StringEnum(GREP_OUTPUT_MODES, {
|
|
104
|
+
description: "Output mode. Defaults to files_with_matches.",
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
107
|
+
"-B": Type.Optional(
|
|
108
|
+
Type.Number({ description: "Lines to show before each match in content mode" }),
|
|
109
|
+
),
|
|
110
|
+
"-A": Type.Optional(
|
|
111
|
+
Type.Number({ description: "Lines to show after each match in content mode" }),
|
|
112
|
+
),
|
|
113
|
+
"-C": Type.Optional(
|
|
114
|
+
Type.Number({ description: "Lines to show before and after each match" }),
|
|
115
|
+
),
|
|
116
|
+
context: Type.Optional(
|
|
117
|
+
Type.Number({ description: "Lines to show before and after each match" }),
|
|
118
|
+
),
|
|
119
|
+
"-n": Type.Optional(
|
|
120
|
+
Type.Boolean({ description: "Show line numbers in content mode; defaults true" }),
|
|
121
|
+
),
|
|
122
|
+
"-i": Type.Optional(Type.Boolean({ description: "Case-insensitive search" })),
|
|
123
|
+
type: Type.Optional(
|
|
124
|
+
Type.String({ description: "ripgrep file type such as js, py, rust, or go" }),
|
|
125
|
+
),
|
|
126
|
+
head_limit: Type.Optional(
|
|
127
|
+
Type.Number({ description: "Limit output to the first N entries after offset" }),
|
|
128
|
+
),
|
|
129
|
+
offset: Type.Optional(Type.Number({ description: "Skip the first N output entries" })),
|
|
130
|
+
multiline: Type.Optional(
|
|
131
|
+
Type.Boolean({ description: "Allow patterns to span multiple lines" }),
|
|
132
|
+
),
|
|
133
|
+
},
|
|
134
|
+
{ additionalProperties: false },
|
|
135
|
+
),
|
|
136
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
137
|
+
if (
|
|
138
|
+
params.offset !== undefined &&
|
|
139
|
+
(!Number.isSafeInteger(params.offset) || params.offset < 0)
|
|
140
|
+
) {
|
|
141
|
+
throw new Error("offset must be a non-negative integer");
|
|
142
|
+
}
|
|
143
|
+
if (
|
|
144
|
+
params.head_limit !== undefined &&
|
|
145
|
+
(!Number.isSafeInteger(params.head_limit) || params.head_limit < 0)
|
|
146
|
+
) {
|
|
147
|
+
throw new Error("head_limit must be a non-negative integer");
|
|
148
|
+
}
|
|
149
|
+
const result = await pi.exec("rg", buildGrepArguments(params, ctx.cwd), { signal });
|
|
150
|
+
throwIfAborted(signal);
|
|
151
|
+
if (result.code !== 0 && result.code !== 1) {
|
|
152
|
+
throw new Error(result.stderr.trim() || `ripgrep exited with code ${result.code}`);
|
|
153
|
+
}
|
|
154
|
+
if (result.code === 1 || result.stdout === "") {
|
|
155
|
+
return { content: [{ type: "text", text: "No files found" }], details: { matches: 0 } };
|
|
156
|
+
}
|
|
157
|
+
const text = pageGrepOutput(result.stdout, params.offset ?? 0, params.head_limit ?? 0);
|
|
158
|
+
return { content: [{ type: "text", text }], details: undefined };
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|