@tt-a1i/openpi 0.1.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/README.md +643 -0
- package/SETUP.md +74 -0
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/openpi-package.png +0 -0
- package/assets/readme-hero-mobile.svg +72 -0
- package/assets/readme-hero.svg +118 -0
- package/assets/readme-runtime-mobile.svg +91 -0
- package/assets/readme-runtime.svg +111 -0
- package/extensions/ask-user/handoff.ts +205 -0
- package/extensions/ask-user/index.ts +1110 -0
- package/extensions/ask-user/limits.ts +89 -0
- package/extensions/ask-user/prompt.ts +76 -0
- package/extensions/background-terminals/index.ts +653 -0
- package/extensions/background-terminals/src/domain.ts +99 -0
- package/extensions/background-terminals/src/manager.ts +989 -0
- package/extensions/background-terminals/src/output.ts +84 -0
- package/extensions/background-terminals/src/prompt.ts +195 -0
- package/extensions/background-terminals/src/result-delivery.ts +43 -0
- package/extensions/background-terminals/src/runtime.ts +36 -0
- package/extensions/background-terminals/src/ui/output-view.ts +55 -0
- package/extensions/background-terminals/src/ui/ps.ts +642 -0
- package/extensions/background-terminals/src/ui/tool-result.ts +146 -0
- package/extensions/background-terminals/src/watch.ts +192 -0
- package/extensions/context-pivot/index.ts +222 -0
- package/extensions/copy-all/index.ts +65 -0
- package/extensions/cron/index.ts +173 -0
- package/extensions/cron/schedule.ts +127 -0
- package/extensions/file-mutation-display/index.ts +105 -0
- package/extensions/file-mutation-display/render.ts +107 -0
- package/extensions/file-search/index.ts +515 -0
- package/extensions/file-search/src/args.ts +129 -0
- package/extensions/file-search/src/binaries.ts +419 -0
- package/extensions/file-search/src/output.ts +142 -0
- package/extensions/file-search/src/process.ts +309 -0
- package/extensions/file-search/src/prompt.ts +53 -0
- package/extensions/git-info/index.ts +272 -0
- package/extensions/git-info/src/changed-files-view.ts +414 -0
- package/extensions/git-info/src/process.ts +107 -0
- package/extensions/git-info/src/refresh-coordinator.ts +13 -0
- package/extensions/git-info/src/runtime.ts +28 -0
- package/extensions/goal/controller.ts +794 -0
- package/extensions/goal/index.ts +521 -0
- package/extensions/goal/prompts.ts +122 -0
- package/extensions/goal/state.ts +763 -0
- package/extensions/goal/ui.ts +158 -0
- package/extensions/model-info/index.ts +234 -0
- package/extensions/plan-mode/bash-policy.ts +313 -0
- package/extensions/plan-mode/index.ts +539 -0
- package/extensions/post-edit/index.ts +129 -0
- package/extensions/sessions/LICENSE.upstream +21 -0
- package/extensions/sessions/git-stats.ts +226 -0
- package/extensions/sessions/index.ts +1092 -0
- package/extensions/sessions/sessions.ts +385 -0
- package/extensions/setup/index.ts +408 -0
- package/extensions/shared/activity-status.ts +65 -0
- package/extensions/shared/below-editor-navigation.ts +343 -0
- package/extensions/shared/child-session.ts +352 -0
- package/extensions/shared/context-utilization.ts +47 -0
- package/extensions/shared/dashboard-state.ts +102 -0
- package/extensions/shared/plan-mode-state.ts +65 -0
- package/extensions/shared/setup-config.ts +971 -0
- package/extensions/shared/subagent-roles.ts +22 -0
- package/extensions/shared/terminal-text.ts +38 -0
- package/extensions/shared/tool-call-timeout.ts +104 -0
- package/extensions/shared/worktree.ts +526 -0
- package/extensions/subagents/index.ts +1225 -0
- package/extensions/subagents/navigation.ts +121 -0
- package/extensions/subagents/src/agent-types.ts +543 -0
- package/extensions/subagents/src/backend.ts +63 -0
- package/extensions/subagents/src/backends/pi.ts +493 -0
- package/extensions/subagents/src/backends/stub.ts +296 -0
- package/extensions/subagents/src/by-the-way.ts +21 -0
- package/extensions/subagents/src/domain.ts +271 -0
- package/extensions/subagents/src/format.ts +48 -0
- package/extensions/subagents/src/manager.ts +769 -0
- package/extensions/subagents/src/prompt.ts +190 -0
- package/extensions/subagents/src/result-delivery.ts +20 -0
- package/extensions/subagents/src/runtime.ts +51 -0
- package/extensions/subagents/src/ui/takeover.ts +615 -0
- package/extensions/subagents/src/ui/transcript.ts +293 -0
- package/extensions/subagents/src/ui/wait-result.ts +89 -0
- package/extensions/suggestions/index.ts +172 -0
- package/extensions/suggestions/src/config.ts +12 -0
- package/extensions/suggestions/src/predictor.ts +147 -0
- package/extensions/suggestions/src/prompt.ts +20 -0
- package/extensions/suggestions/src/transcript.ts +233 -0
- package/extensions/suggestions/src/ui.ts +224 -0
- package/extensions/tasks/index.ts +512 -0
- package/extensions/tasks/tasks.ts +649 -0
- package/extensions/tasks/ui.ts +421 -0
- package/extensions/turn-time/index.ts +61 -0
- package/extensions/ui-customization/footer.ts +512 -0
- package/extensions/ui-customization/index.ts +217 -0
- package/extensions/workflows/acceptance.ts +298 -0
- package/extensions/workflows/artifacts.ts +225 -0
- package/extensions/workflows/controller.ts +210 -0
- package/extensions/workflows/dashboard.ts +1226 -0
- package/extensions/workflows/index.ts +1884 -0
- package/extensions/workflows/journal.ts +188 -0
- package/extensions/workflows/meta.ts +250 -0
- package/extensions/workflows/model.ts +423 -0
- package/extensions/workflows/navigation.ts +93 -0
- package/extensions/workflows/prompt.ts +212 -0
- package/extensions/workflows/replay-safety.ts +577 -0
- package/extensions/workflows/runner.ts +786 -0
- package/extensions/workflows/sandbox-child.cjs +402 -0
- package/extensions/workflows/sandbox.ts +397 -0
- package/extensions/workflows/serialization.ts +162 -0
- package/extensions/workflows/worktree-handoff.ts +216 -0
- package/package.json +87 -0
- package/scripts/prepare-effect-tsgo.mjs +16 -0
- package/skills/background-terminals/SKILL.md +30 -0
- package/skills/subagents/SKILL.md +15 -0
- package/themes/github-dark-default.json +89 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file-search — first-class `fd` and `rg` tools for pi.
|
|
3
|
+
*
|
|
4
|
+
* On session start the extension resolves a usable binary for each tool:
|
|
5
|
+
* a normally installed system binary is preferred (silently), then an
|
|
6
|
+
* existing fallback in this repo's `bin/` directory (silently), and only
|
|
7
|
+
* when neither exists is an official release downloaded into `bin/` — the
|
|
8
|
+
* single case that shows a UI notification. Tools await that initialization
|
|
9
|
+
* before executing, and report a clear error if it failed.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { NodeServices } from "@effect/platform-node";
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import type {
|
|
16
|
+
AgentToolResult,
|
|
17
|
+
ExtensionAPI,
|
|
18
|
+
ExtensionContext,
|
|
19
|
+
} from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
21
|
+
import { Cause, Data, Effect, Exit } from "effect";
|
|
22
|
+
import { Type, type Static } from "typebox";
|
|
23
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
24
|
+
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
|
|
25
|
+
import {
|
|
26
|
+
buildFdArgs,
|
|
27
|
+
buildRgArgs,
|
|
28
|
+
FD_MAX_DEPTH_LIMIT,
|
|
29
|
+
FD_MAX_LIMIT,
|
|
30
|
+
RG_MAX_CONTEXT,
|
|
31
|
+
RG_MAX_COUNT_LIMIT,
|
|
32
|
+
} from "./src/args.ts";
|
|
33
|
+
import {
|
|
34
|
+
currentTarget,
|
|
35
|
+
liveBinaryEnv,
|
|
36
|
+
repositoryBinDir,
|
|
37
|
+
resolveBinary,
|
|
38
|
+
TOOL_SPECS,
|
|
39
|
+
type BinaryEnv,
|
|
40
|
+
type BinarySource,
|
|
41
|
+
type PlatformTarget,
|
|
42
|
+
type ResolvedBinary,
|
|
43
|
+
} from "./src/binaries.ts";
|
|
44
|
+
import { formatCapturedOutput, type CapturedOutput } from "./src/output.ts";
|
|
45
|
+
import {
|
|
46
|
+
FD_PARAMETER_DESCRIPTIONS,
|
|
47
|
+
FD_PROMPT_GUIDELINES,
|
|
48
|
+
FD_PROMPT_SNIPPET,
|
|
49
|
+
FD_TOOL_DESCRIPTION,
|
|
50
|
+
RG_PARAMETER_DESCRIPTIONS,
|
|
51
|
+
RG_PROMPT_GUIDELINES,
|
|
52
|
+
RG_PROMPT_SNIPPET,
|
|
53
|
+
RG_TOOL_DESCRIPTION,
|
|
54
|
+
} from "./src/prompt.ts";
|
|
55
|
+
import { discardCapturedOutput, executeSearchProcess } from "./src/process.ts";
|
|
56
|
+
|
|
57
|
+
export function makeBinaryInitializers(
|
|
58
|
+
binDir: string,
|
|
59
|
+
target: PlatformTarget,
|
|
60
|
+
env: BinaryEnv,
|
|
61
|
+
) {
|
|
62
|
+
return {
|
|
63
|
+
fd: Effect.runSync(
|
|
64
|
+
Effect.cached(resolveBinary(TOOL_SPECS.fd, binDir, target, env)),
|
|
65
|
+
),
|
|
66
|
+
rg: Effect.runSync(
|
|
67
|
+
Effect.cached(resolveBinary(TOOL_SPECS.rg, binDir, target, env)),
|
|
68
|
+
),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Human-readable install notice, shown only for fresh downloads. */
|
|
73
|
+
export function installNotifications(binaries: readonly ResolvedBinary[]) {
|
|
74
|
+
return binaries
|
|
75
|
+
.filter((binary) => binary.source === "installed")
|
|
76
|
+
.map(
|
|
77
|
+
(binary) =>
|
|
78
|
+
`file-search: no system ${binary.tool} found — downloaded ${binary.tool} ${binary.version ?? ""}`.trimEnd() +
|
|
79
|
+
` to ${repositoryBinDir()}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class SearchError extends Data.TaggedError("SearchError")<{
|
|
84
|
+
readonly message: string;
|
|
85
|
+
}> {}
|
|
86
|
+
|
|
87
|
+
interface SearchOutcome {
|
|
88
|
+
readonly output: CapturedOutput;
|
|
89
|
+
readonly noMatches: boolean;
|
|
90
|
+
readonly binarySource: BinarySource;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface FdToolDetails {
|
|
94
|
+
readonly binarySource: BinarySource;
|
|
95
|
+
readonly matchCount: number;
|
|
96
|
+
readonly truncated: boolean;
|
|
97
|
+
readonly fullOutputPath?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface RgToolDetails {
|
|
101
|
+
readonly binarySource: BinarySource;
|
|
102
|
+
readonly outputLines: number;
|
|
103
|
+
readonly truncated: boolean;
|
|
104
|
+
readonly fullOutputPath?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const EXEC_TIMEOUT_MS = 60_000;
|
|
108
|
+
|
|
109
|
+
const displayArgument = (value: string) =>
|
|
110
|
+
sanitizeTerminalText(value).replace(/\s+/gu, " ").trim();
|
|
111
|
+
|
|
112
|
+
function causeMessage<E>(cause: Cause.Cause<E>) {
|
|
113
|
+
const [first] = Cause.prettyErrors(cause);
|
|
114
|
+
return first?.message ?? Cause.pretty(cause);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function unwrapToolExit<A, E>(exit: Exit.Exit<A, E>, tool: "fd" | "rg") {
|
|
118
|
+
if (Exit.isSuccess(exit)) return exit.value;
|
|
119
|
+
if (Cause.hasInterruptsOnly(exit.cause)) {
|
|
120
|
+
throw new Error(`${tool} search was cancelled.`);
|
|
121
|
+
}
|
|
122
|
+
throw new Error(causeMessage(exit.cause));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export default function fileSearchTools(pi: ExtensionAPI) {
|
|
126
|
+
let notified = false;
|
|
127
|
+
const resultDirectories = new Set<string>();
|
|
128
|
+
|
|
129
|
+
const rememberOutput = (output: CapturedOutput) => {
|
|
130
|
+
if (output.fullOutputPath) {
|
|
131
|
+
resultDirectories.add(path.dirname(output.fullOutputPath));
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const binDir = repositoryBinDir();
|
|
136
|
+
const target = currentTarget();
|
|
137
|
+
const initializers = makeBinaryInitializers(binDir, target, liveBinaryEnv);
|
|
138
|
+
|
|
139
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
140
|
+
const exit = await Effect.runPromiseExit(
|
|
141
|
+
Effect.gen(function* () {
|
|
142
|
+
const initialized = yield* Effect.all(
|
|
143
|
+
{
|
|
144
|
+
fd: Effect.exit(initializers.fd),
|
|
145
|
+
rg: Effect.exit(initializers.rg),
|
|
146
|
+
},
|
|
147
|
+
{ concurrency: "unbounded" },
|
|
148
|
+
);
|
|
149
|
+
if (!ctx.hasUI || notified) return;
|
|
150
|
+
|
|
151
|
+
notified = true;
|
|
152
|
+
for (const tool of ["fd", "rg"] as const) {
|
|
153
|
+
const toolExit = initialized[tool];
|
|
154
|
+
if (Exit.isSuccess(toolExit)) {
|
|
155
|
+
for (const message of installNotifications([toolExit.value])) {
|
|
156
|
+
ctx.ui.notify(message, "info");
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
ctx.ui.notify(
|
|
160
|
+
`file-search ${tool} setup failed: ${causeMessage(toolExit.cause)}`,
|
|
161
|
+
"error",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
if (Exit.isFailure(exit) && ctx.hasUI && !notified) {
|
|
169
|
+
notified = true;
|
|
170
|
+
ctx.ui.notify(
|
|
171
|
+
`file-search setup failed: ${causeMessage(exit.cause)}`,
|
|
172
|
+
"error",
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
/** Await init, stream the binary output to disk, and classify its exit. */
|
|
178
|
+
function runSearch(tool: "fd" | "rg", args: string[], ctx: ExtensionContext) {
|
|
179
|
+
return Effect.gen(function* () {
|
|
180
|
+
const binary = yield* initializers[tool];
|
|
181
|
+
const result = yield* executeSearchProcess({
|
|
182
|
+
command: binary.command,
|
|
183
|
+
args,
|
|
184
|
+
cwd: ctx.cwd,
|
|
185
|
+
tempPrefix: `pi-${tool}-`,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (result.output.captureLimitExceeded) {
|
|
189
|
+
return {
|
|
190
|
+
output: result.output,
|
|
191
|
+
noMatches: false,
|
|
192
|
+
binarySource: binary.source,
|
|
193
|
+
} satisfies SearchOutcome;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ripgrep exits 1 for "no matches"; fd exits 0 even with no results.
|
|
197
|
+
if (tool === "rg" && result.code === 1 && result.output.lineCount === 0) {
|
|
198
|
+
return {
|
|
199
|
+
output: result.output,
|
|
200
|
+
noMatches: true,
|
|
201
|
+
binarySource: binary.source,
|
|
202
|
+
} satisfies SearchOutcome;
|
|
203
|
+
}
|
|
204
|
+
if (result.code !== 0) {
|
|
205
|
+
yield* discardCapturedOutput(result.output);
|
|
206
|
+
const detail = result.stderr.trim() || `exit code ${result.code}`;
|
|
207
|
+
return yield* new SearchError({ message: `${tool} failed: ${detail}` });
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
output: result.output,
|
|
211
|
+
noMatches: result.output.lineCount === 0,
|
|
212
|
+
binarySource: binary.source,
|
|
213
|
+
} satisfies SearchOutcome;
|
|
214
|
+
}).pipe(
|
|
215
|
+
Effect.timeout(EXEC_TIMEOUT_MS),
|
|
216
|
+
Effect.mapError((error) => {
|
|
217
|
+
if (error instanceof SearchError) return error;
|
|
218
|
+
return new SearchError({
|
|
219
|
+
message:
|
|
220
|
+
error._tag === "TimeoutError"
|
|
221
|
+
? `${tool} timed out.`
|
|
222
|
+
: error instanceof Error
|
|
223
|
+
? error.message
|
|
224
|
+
: String(error),
|
|
225
|
+
});
|
|
226
|
+
}),
|
|
227
|
+
Effect.provide(NodeServices.layer),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
pi.on("session_shutdown", () => {
|
|
232
|
+
for (const directory of resultDirectories) {
|
|
233
|
+
try {
|
|
234
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
235
|
+
} catch {
|
|
236
|
+
// Temporary search artifacts are best-effort cleanup.
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
resultDirectories.clear();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
pi.registerTool<ReturnType<typeof fdParameters>, FdToolDetails>({
|
|
243
|
+
name: "fd",
|
|
244
|
+
label: "Find Files",
|
|
245
|
+
description: FD_TOOL_DESCRIPTION,
|
|
246
|
+
promptSnippet: FD_PROMPT_SNIPPET,
|
|
247
|
+
promptGuidelines: FD_PROMPT_GUIDELINES,
|
|
248
|
+
parameters: fdParameters(),
|
|
249
|
+
|
|
250
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
251
|
+
const exit = await Effect.runPromiseExit(
|
|
252
|
+
Effect.gen(function* () {
|
|
253
|
+
const outcome = yield* runSearch("fd", buildFdArgs(params), ctx);
|
|
254
|
+
if (outcome.noMatches) {
|
|
255
|
+
return {
|
|
256
|
+
content: [{ type: "text", text: "No files found" }],
|
|
257
|
+
details: {
|
|
258
|
+
binarySource: outcome.binarySource,
|
|
259
|
+
matchCount: 0,
|
|
260
|
+
truncated: false,
|
|
261
|
+
},
|
|
262
|
+
} satisfies AgentToolResult<FdToolDetails>;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
rememberOutput(outcome.output);
|
|
266
|
+
const formatted = formatCapturedOutput(outcome.output);
|
|
267
|
+
return {
|
|
268
|
+
content: [{ type: "text", text: formatted.text }],
|
|
269
|
+
details: {
|
|
270
|
+
binarySource: outcome.binarySource,
|
|
271
|
+
matchCount: formatted.lineCount,
|
|
272
|
+
truncated: formatted.truncated,
|
|
273
|
+
fullOutputPath: formatted.fullOutputPath,
|
|
274
|
+
},
|
|
275
|
+
} satisfies AgentToolResult<FdToolDetails>;
|
|
276
|
+
}),
|
|
277
|
+
signal ? { signal } : undefined,
|
|
278
|
+
);
|
|
279
|
+
return unwrapToolExit(exit, "fd");
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
renderCall(args, theme) {
|
|
283
|
+
let text = theme.fg("toolTitle", theme.bold("fd "));
|
|
284
|
+
text += theme.fg(
|
|
285
|
+
"accent",
|
|
286
|
+
args.pattern ? `"${displayArgument(args.pattern)}"` : "(all)",
|
|
287
|
+
);
|
|
288
|
+
if (args.path)
|
|
289
|
+
text += theme.fg("muted", ` in ${displayArgument(args.path)}`);
|
|
290
|
+
const flags = [
|
|
291
|
+
args.type && `type=${args.type}`,
|
|
292
|
+
args.extension && `ext=${displayArgument(args.extension)}`,
|
|
293
|
+
args.glob && "glob",
|
|
294
|
+
args.hidden && "hidden",
|
|
295
|
+
args.max_depth !== undefined && `depth≤${args.max_depth}`,
|
|
296
|
+
].filter((flag): flag is string => typeof flag === "string");
|
|
297
|
+
if (flags.length > 0) text += " " + theme.fg("dim", flags.join(" "));
|
|
298
|
+
return new Text(text, 0, 0);
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
302
|
+
if (isPartial) return new Text(theme.fg("warning", "Searching..."), 0, 0);
|
|
303
|
+
const details = result.details;
|
|
304
|
+
if (!details || details.matchCount === 0) {
|
|
305
|
+
return new Text(theme.fg("dim", "No files found"), 0, 0);
|
|
306
|
+
}
|
|
307
|
+
let text = theme.fg(
|
|
308
|
+
"success",
|
|
309
|
+
`${details.matchCount} ${details.matchCount === 1 ? "entry" : "entries"}`,
|
|
310
|
+
);
|
|
311
|
+
if (details.truncated) text += theme.fg("warning", " (truncated)");
|
|
312
|
+
if (expanded)
|
|
313
|
+
text += expandedPreview(result, details.fullOutputPath, theme);
|
|
314
|
+
return new Text(text, 0, 0);
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
pi.registerTool<ReturnType<typeof rgParameters>, RgToolDetails>({
|
|
319
|
+
name: "rg",
|
|
320
|
+
label: "Search Content",
|
|
321
|
+
description: RG_TOOL_DESCRIPTION,
|
|
322
|
+
promptSnippet: RG_PROMPT_SNIPPET,
|
|
323
|
+
promptGuidelines: RG_PROMPT_GUIDELINES,
|
|
324
|
+
parameters: rgParameters(),
|
|
325
|
+
prepareArguments(args) {
|
|
326
|
+
if (!args || typeof args !== "object") {
|
|
327
|
+
return args as Static<ReturnType<typeof rgParameters>>;
|
|
328
|
+
}
|
|
329
|
+
const input = args as {
|
|
330
|
+
limit?: unknown;
|
|
331
|
+
max_matches_per_file?: unknown;
|
|
332
|
+
};
|
|
333
|
+
if (
|
|
334
|
+
input.max_matches_per_file === undefined &&
|
|
335
|
+
typeof input.limit === "number"
|
|
336
|
+
) {
|
|
337
|
+
const { limit, ...rest } = input;
|
|
338
|
+
return {
|
|
339
|
+
...rest,
|
|
340
|
+
max_matches_per_file: limit,
|
|
341
|
+
} as Static<ReturnType<typeof rgParameters>>;
|
|
342
|
+
}
|
|
343
|
+
return args as Static<ReturnType<typeof rgParameters>>;
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
347
|
+
const exit = await Effect.runPromiseExit(
|
|
348
|
+
Effect.gen(function* () {
|
|
349
|
+
const outcome = yield* runSearch("rg", buildRgArgs(params), ctx);
|
|
350
|
+
if (outcome.noMatches) {
|
|
351
|
+
return {
|
|
352
|
+
content: [{ type: "text", text: "No matches found" }],
|
|
353
|
+
details: {
|
|
354
|
+
binarySource: outcome.binarySource,
|
|
355
|
+
outputLines: 0,
|
|
356
|
+
truncated: false,
|
|
357
|
+
},
|
|
358
|
+
} satisfies AgentToolResult<RgToolDetails>;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
rememberOutput(outcome.output);
|
|
362
|
+
const formatted = formatCapturedOutput(outcome.output);
|
|
363
|
+
return {
|
|
364
|
+
content: [{ type: "text", text: formatted.text }],
|
|
365
|
+
details: {
|
|
366
|
+
binarySource: outcome.binarySource,
|
|
367
|
+
outputLines: formatted.lineCount,
|
|
368
|
+
truncated: formatted.truncated,
|
|
369
|
+
fullOutputPath: formatted.fullOutputPath,
|
|
370
|
+
},
|
|
371
|
+
} satisfies AgentToolResult<RgToolDetails>;
|
|
372
|
+
}),
|
|
373
|
+
signal ? { signal } : undefined,
|
|
374
|
+
);
|
|
375
|
+
return unwrapToolExit(exit, "rg");
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
renderCall(args, theme) {
|
|
379
|
+
let text = theme.fg("toolTitle", theme.bold("rg "));
|
|
380
|
+
text += theme.fg("accent", `"${displayArgument(args.pattern)}"`);
|
|
381
|
+
if (args.path)
|
|
382
|
+
text += theme.fg("muted", ` in ${displayArgument(args.path)}`);
|
|
383
|
+
const flags = [
|
|
384
|
+
args.glob && `glob=${displayArgument(args.glob)}`,
|
|
385
|
+
args.file_type && `type=${displayArgument(args.file_type)}`,
|
|
386
|
+
args.fixed_strings && "literal",
|
|
387
|
+
args.hidden && "hidden",
|
|
388
|
+
args.context !== undefined && `ctx=${args.context}`,
|
|
389
|
+
].filter((flag): flag is string => typeof flag === "string");
|
|
390
|
+
if (flags.length > 0) text += " " + theme.fg("dim", flags.join(" "));
|
|
391
|
+
return new Text(text, 0, 0);
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
395
|
+
if (isPartial) return new Text(theme.fg("warning", "Searching..."), 0, 0);
|
|
396
|
+
const details = result.details;
|
|
397
|
+
if (!details || details.outputLines === 0) {
|
|
398
|
+
return new Text(theme.fg("dim", "No matches found"), 0, 0);
|
|
399
|
+
}
|
|
400
|
+
let text = theme.fg(
|
|
401
|
+
"success",
|
|
402
|
+
`${details.outputLines} output ${details.outputLines === 1 ? "line" : "lines"}`,
|
|
403
|
+
);
|
|
404
|
+
if (details.truncated) text += theme.fg("warning", " (truncated)");
|
|
405
|
+
if (expanded)
|
|
406
|
+
text += expandedPreview(result, details.fullOutputPath, theme);
|
|
407
|
+
return new Text(text, 0, 0);
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const PREVIEW_LINES = 20;
|
|
413
|
+
|
|
414
|
+
interface ThemeLike {
|
|
415
|
+
fg(color: string, text: string): string;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function expandedPreview(
|
|
419
|
+
result: { content: { type: string; text?: string }[] },
|
|
420
|
+
fullOutputPath: string | undefined,
|
|
421
|
+
theme: ThemeLike,
|
|
422
|
+
) {
|
|
423
|
+
let text = "";
|
|
424
|
+
const content = result.content[0];
|
|
425
|
+
if (content?.type === "text" && content.text) {
|
|
426
|
+
const lines = sanitizeTerminalText(content.text).split("\n");
|
|
427
|
+
for (const line of lines.slice(0, PREVIEW_LINES)) {
|
|
428
|
+
text += `\n${theme.fg("dim", line)}`;
|
|
429
|
+
}
|
|
430
|
+
if (lines.length > PREVIEW_LINES) {
|
|
431
|
+
text += `\n${theme.fg("muted", `... ${lines.length - PREVIEW_LINES} more lines`)}`;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (fullOutputPath) {
|
|
435
|
+
text += `\n${theme.fg("dim", `Full output: ${displayArgument(fullOutputPath)}`)}`;
|
|
436
|
+
}
|
|
437
|
+
return text;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function fdParameters() {
|
|
441
|
+
return Type.Object({
|
|
442
|
+
pattern: Type.Optional(
|
|
443
|
+
Type.String({ description: FD_PARAMETER_DESCRIPTIONS.pattern }),
|
|
444
|
+
),
|
|
445
|
+
path: Type.Optional(
|
|
446
|
+
Type.String({ description: FD_PARAMETER_DESCRIPTIONS.path }),
|
|
447
|
+
),
|
|
448
|
+
type: Type.Optional(
|
|
449
|
+
StringEnum(["file", "directory", "symlink"] as const, {
|
|
450
|
+
description: FD_PARAMETER_DESCRIPTIONS.type,
|
|
451
|
+
}),
|
|
452
|
+
),
|
|
453
|
+
extension: Type.Optional(
|
|
454
|
+
Type.String({ description: FD_PARAMETER_DESCRIPTIONS.extension }),
|
|
455
|
+
),
|
|
456
|
+
glob: Type.Optional(
|
|
457
|
+
Type.Boolean({ description: FD_PARAMETER_DESCRIPTIONS.glob }),
|
|
458
|
+
),
|
|
459
|
+
hidden: Type.Optional(
|
|
460
|
+
Type.Boolean({ description: FD_PARAMETER_DESCRIPTIONS.hidden }),
|
|
461
|
+
),
|
|
462
|
+
max_depth: Type.Optional(
|
|
463
|
+
Type.Integer({
|
|
464
|
+
description: FD_PARAMETER_DESCRIPTIONS.max_depth,
|
|
465
|
+
minimum: 1,
|
|
466
|
+
maximum: FD_MAX_DEPTH_LIMIT,
|
|
467
|
+
}),
|
|
468
|
+
),
|
|
469
|
+
limit: Type.Optional(
|
|
470
|
+
Type.Integer({
|
|
471
|
+
description: FD_PARAMETER_DESCRIPTIONS.limit,
|
|
472
|
+
minimum: 1,
|
|
473
|
+
maximum: FD_MAX_LIMIT,
|
|
474
|
+
}),
|
|
475
|
+
),
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function rgParameters() {
|
|
480
|
+
return Type.Object({
|
|
481
|
+
pattern: Type.String({ description: RG_PARAMETER_DESCRIPTIONS.pattern }),
|
|
482
|
+
path: Type.Optional(
|
|
483
|
+
Type.String({ description: RG_PARAMETER_DESCRIPTIONS.path }),
|
|
484
|
+
),
|
|
485
|
+
glob: Type.Optional(
|
|
486
|
+
Type.String({ description: RG_PARAMETER_DESCRIPTIONS.glob }),
|
|
487
|
+
),
|
|
488
|
+
file_type: Type.Optional(
|
|
489
|
+
Type.String({ description: RG_PARAMETER_DESCRIPTIONS.file_type }),
|
|
490
|
+
),
|
|
491
|
+
case_sensitive: Type.Optional(
|
|
492
|
+
Type.Boolean({ description: RG_PARAMETER_DESCRIPTIONS.case_sensitive }),
|
|
493
|
+
),
|
|
494
|
+
fixed_strings: Type.Optional(
|
|
495
|
+
Type.Boolean({ description: RG_PARAMETER_DESCRIPTIONS.fixed_strings }),
|
|
496
|
+
),
|
|
497
|
+
hidden: Type.Optional(
|
|
498
|
+
Type.Boolean({ description: RG_PARAMETER_DESCRIPTIONS.hidden }),
|
|
499
|
+
),
|
|
500
|
+
context: Type.Optional(
|
|
501
|
+
Type.Integer({
|
|
502
|
+
description: RG_PARAMETER_DESCRIPTIONS.context,
|
|
503
|
+
minimum: 0,
|
|
504
|
+
maximum: RG_MAX_CONTEXT,
|
|
505
|
+
}),
|
|
506
|
+
),
|
|
507
|
+
max_matches_per_file: Type.Optional(
|
|
508
|
+
Type.Integer({
|
|
509
|
+
description: RG_PARAMETER_DESCRIPTIONS.max_matches_per_file,
|
|
510
|
+
minimum: 1,
|
|
511
|
+
maximum: RG_MAX_COUNT_LIMIT,
|
|
512
|
+
}),
|
|
513
|
+
),
|
|
514
|
+
});
|
|
515
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure CLI argument construction for the fd and rg tools.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is synchronous and side-effect free so the exact argv
|
|
5
|
+
* passed to pi.exec can be asserted in tests. Patterns are always placed
|
|
6
|
+
* after a `--` separator so user-controlled input can never be parsed as a
|
|
7
|
+
* flag, and paths are normalized (leading `@`, `~` expansion) before use.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
export const FD_DEFAULT_LIMIT = 1000;
|
|
14
|
+
export const FD_MAX_LIMIT = 10_000;
|
|
15
|
+
export const FD_MAX_DEPTH_LIMIT = 64;
|
|
16
|
+
export const RG_DEFAULT_COUNT_LIMIT = 100;
|
|
17
|
+
export const RG_MAX_COUNT_LIMIT = 1000;
|
|
18
|
+
export const RG_MAX_CONTEXT = 20;
|
|
19
|
+
|
|
20
|
+
/** Some models prefix path arguments with @; built-in tools strip it, so do we. */
|
|
21
|
+
export function normalizeSearchPath(raw: string) {
|
|
22
|
+
let path = raw.trim();
|
|
23
|
+
if (path.startsWith("@")) path = path.slice(1);
|
|
24
|
+
if (path === "~") return homedir();
|
|
25
|
+
if (path.startsWith("~/")) return join(homedir(), path.slice(2));
|
|
26
|
+
return path;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function clamp(value: number, minimum: number, maximum: number) {
|
|
30
|
+
return Math.min(maximum, Math.max(minimum, Math.floor(value)));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function optionalPath(raw: string | undefined) {
|
|
34
|
+
if (raw === undefined) return undefined;
|
|
35
|
+
const normalized = normalizeSearchPath(raw);
|
|
36
|
+
return normalized === "" ? undefined : normalized;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type FdEntryType = "file" | "directory" | "symlink";
|
|
40
|
+
|
|
41
|
+
export interface FdToolParams {
|
|
42
|
+
pattern?: string;
|
|
43
|
+
path?: string;
|
|
44
|
+
type?: FdEntryType;
|
|
45
|
+
extension?: string;
|
|
46
|
+
glob?: boolean;
|
|
47
|
+
hidden?: boolean;
|
|
48
|
+
max_depth?: number;
|
|
49
|
+
limit?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const FD_TYPE_FLAGS: Record<FdEntryType, string> = {
|
|
53
|
+
file: "f",
|
|
54
|
+
directory: "d",
|
|
55
|
+
symlink: "l",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export function buildFdArgs(params: FdToolParams) {
|
|
59
|
+
const args = ["--color=never"];
|
|
60
|
+
if (params.hidden) args.push("--hidden");
|
|
61
|
+
if (params.glob) args.push("--glob");
|
|
62
|
+
if (params.type) args.push("--type", FD_TYPE_FLAGS[params.type]);
|
|
63
|
+
if (params.extension) {
|
|
64
|
+
args.push("--extension", params.extension.replace(/^\.+/, ""));
|
|
65
|
+
}
|
|
66
|
+
if (params.max_depth !== undefined) {
|
|
67
|
+
args.push(
|
|
68
|
+
"--max-depth",
|
|
69
|
+
String(clamp(params.max_depth, 1, FD_MAX_DEPTH_LIMIT)),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
args.push(
|
|
73
|
+
"--max-results",
|
|
74
|
+
String(clamp(params.limit ?? FD_DEFAULT_LIMIT, 1, FD_MAX_LIMIT)),
|
|
75
|
+
);
|
|
76
|
+
// An empty pattern matches everything, which keeps `path` usable without a pattern.
|
|
77
|
+
args.push("--", params.pattern ?? "");
|
|
78
|
+
const path = optionalPath(params.path);
|
|
79
|
+
if (path) args.push(path);
|
|
80
|
+
return args;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface RgToolParams {
|
|
84
|
+
pattern: string;
|
|
85
|
+
path?: string;
|
|
86
|
+
glob?: string;
|
|
87
|
+
file_type?: string;
|
|
88
|
+
case_sensitive?: boolean;
|
|
89
|
+
fixed_strings?: boolean;
|
|
90
|
+
hidden?: boolean;
|
|
91
|
+
context?: number;
|
|
92
|
+
/** Current public name. */
|
|
93
|
+
max_matches_per_file?: number;
|
|
94
|
+
/** Legacy stored tool calls; normalized before execution. */
|
|
95
|
+
limit?: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function buildRgArgs(params: RgToolParams) {
|
|
99
|
+
const args = [
|
|
100
|
+
"--line-number",
|
|
101
|
+
"--color=never",
|
|
102
|
+
"--no-heading",
|
|
103
|
+
"--with-filename",
|
|
104
|
+
];
|
|
105
|
+
if (params.case_sensitive === true) args.push("--case-sensitive");
|
|
106
|
+
else if (params.case_sensitive === false) args.push("--ignore-case");
|
|
107
|
+
else args.push("--smart-case");
|
|
108
|
+
if (params.fixed_strings) args.push("--fixed-strings");
|
|
109
|
+
if (params.hidden) args.push("--hidden");
|
|
110
|
+
if (params.context !== undefined) {
|
|
111
|
+
args.push("--context", String(clamp(params.context, 0, RG_MAX_CONTEXT)));
|
|
112
|
+
}
|
|
113
|
+
if (params.glob) args.push("--glob", params.glob);
|
|
114
|
+
if (params.file_type) args.push("--type", params.file_type);
|
|
115
|
+
args.push(
|
|
116
|
+
"--max-count",
|
|
117
|
+
String(
|
|
118
|
+
clamp(
|
|
119
|
+
params.max_matches_per_file ?? params.limit ?? RG_DEFAULT_COUNT_LIMIT,
|
|
120
|
+
1,
|
|
121
|
+
RG_MAX_COUNT_LIMIT,
|
|
122
|
+
),
|
|
123
|
+
),
|
|
124
|
+
);
|
|
125
|
+
args.push("--", params.pattern);
|
|
126
|
+
const path = optionalPath(params.path);
|
|
127
|
+
if (path) args.push(path);
|
|
128
|
+
return args;
|
|
129
|
+
}
|