@oh-my-pi/pi-coding-agent 3.30.0 → 3.32.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 +85 -0
- package/package.json +5 -5
- package/src/cli/args.ts +4 -0
- package/src/core/agent-session.ts +29 -2
- package/src/core/bash-executor.ts +2 -1
- package/src/core/custom-commands/bundled/review/index.ts +367 -14
- package/src/core/custom-commands/bundled/wt/index.ts +1 -1
- package/src/core/sdk.ts +10 -2
- package/src/core/session-manager.ts +158 -246
- package/src/core/session-storage.ts +379 -0
- package/src/core/settings-manager.ts +155 -4
- package/src/core/slash-commands.ts +39 -13
- package/src/core/system-prompt.ts +62 -64
- package/src/core/tools/ask.ts +5 -4
- package/src/core/tools/bash-interceptor.ts +26 -61
- package/src/core/tools/bash.ts +13 -8
- package/src/core/tools/edit-diff.ts +11 -4
- package/src/core/tools/edit.ts +7 -13
- package/src/core/tools/find.ts +111 -50
- package/src/core/tools/gemini-image.ts +128 -147
- package/src/core/tools/grep.ts +397 -415
- package/src/core/tools/index.test.ts +5 -1
- package/src/core/tools/index.ts +8 -4
- package/src/core/tools/ls.ts +12 -10
- package/src/core/tools/lsp/client.ts +84 -19
- package/src/core/tools/lsp/config.ts +205 -656
- package/src/core/tools/lsp/defaults.json +465 -0
- package/src/core/tools/lsp/index.ts +72 -35
- package/src/core/tools/lsp/rust-analyzer.ts +49 -10
- package/src/core/tools/lsp/types.ts +1 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/read.ts +150 -74
- package/src/core/tools/render-utils.ts +70 -10
- package/src/core/tools/review.ts +38 -126
- package/src/core/tools/task/artifacts.ts +5 -4
- package/src/core/tools/task/commands.ts +4 -0
- package/src/core/tools/task/executor.ts +94 -83
- package/src/core/tools/task/index.ts +130 -92
- package/src/core/tools/task/parallel.ts +30 -3
- package/src/core/tools/task/render.ts +85 -39
- package/src/core/tools/task/types.ts +15 -6
- package/src/core/tools/task/worker.ts +124 -89
- package/src/core/tools/web-fetch.ts +112 -377
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/artifacthub.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/arxiv.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/aur.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/biorxiv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/bluesky.ts +10 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/brew.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/cheatsh.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/chocolatey.ts +6 -1
- package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
- package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
- package/src/core/tools/web-scrapers/clojars.ts +180 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/coingecko.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/crates-io.ts +7 -2
- package/src/core/tools/web-scrapers/crossref.ts +149 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/devto.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/discogs.ts +6 -1
- package/src/core/tools/web-scrapers/discourse.ts +221 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/dockerhub.ts +7 -3
- package/src/core/tools/web-scrapers/fdroid.ts +158 -0
- package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
- package/src/core/tools/web-scrapers/flathub.ts +239 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/github-gist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/github.ts +63 -32
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/gitlab.ts +31 -19
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/go-pkg.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackage.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackernews.ts +18 -18
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hex.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/huggingface.ts +10 -10
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/iacr.ts +8 -4
- package/src/core/tools/web-scrapers/index.ts +250 -0
- package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
- package/src/core/tools/web-scrapers/lemmy.ts +220 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/lobsters.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mastodon.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/maven.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mdn.ts +2 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/metacpan.ts +13 -7
- package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/npm.ts +12 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nuget.ts +9 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nvd.ts +6 -1
- package/src/core/tools/web-scrapers/ollama.ts +267 -0
- package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/opencorporates.ts +2 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/openlibrary.ts +18 -12
- package/src/core/tools/web-scrapers/orcid.ts +299 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/osv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/packagist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pub-dev.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pubmed.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pypi.ts +7 -3
- package/src/core/tools/web-scrapers/rawg.ts +124 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/readthedocs.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/reddit.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/repology.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rfc.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rubygems.ts +6 -1
- package/src/core/tools/web-scrapers/searchcode.ts +217 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/sec-edgar.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/semantic-scholar.ts +2 -2
- package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
- package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
- package/src/core/tools/web-scrapers/spdx.ts +121 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/spotify.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackoverflow.ts +3 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/terraform.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/tldr.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/twitter.ts +15 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/types.ts +98 -27
- package/src/core/tools/web-scrapers/utils.ts +162 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/vimeo.ts +3 -3
- package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
- package/src/core/tools/web-scrapers/w3c.ts +163 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikidata.ts +13 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.ts +72 -20
- package/src/core/tools/write.ts +21 -18
- package/src/core/voice.ts +3 -2
- package/src/lib/worktree/collapse.ts +2 -1
- package/src/lib/worktree/git.ts +2 -18
- package/src/main.ts +59 -3
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
- package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
- package/src/modes/interactive/components/hook-editor.ts +2 -1
- package/src/modes/interactive/components/model-selector.ts +19 -4
- package/src/modes/interactive/interactive-mode.ts +41 -63
- package/src/modes/interactive/theme/theme.ts +58 -58
- package/src/modes/rpc/rpc-mode.ts +10 -9
- package/src/prompts/review-request.md +27 -0
- package/src/prompts/reviewer.md +64 -68
- package/src/prompts/tools/output.md +22 -3
- package/src/prompts/tools/task.md +32 -33
- package/src/utils/clipboard.ts +2 -1
- package/examples/extensions/subagent/agents/reviewer.md +0 -35
- package/src/core/tools/web-fetch-handlers/index.ts +0 -69
- package/src/core/tools/web-fetch-handlers/utils.ts +0 -91
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/academic.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/business.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/dev-platforms.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/documentation.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/finance-media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/git-hosting.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers-2.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-registries.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/research.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/security.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social-extended.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackexchange.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/standards.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.test.ts +0 -0
package/src/core/tools/read.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import type { ImageContent, TextContent } from "@mariozechner/pi-ai";
|
|
4
3
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
5
4
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
6
5
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
7
6
|
import { Type } from "@sinclair/typebox";
|
|
8
|
-
import { globSync } from "glob";
|
|
9
7
|
import { getLanguageFromPath, highlightCode, type Theme } from "../../modes/interactive/theme/theme";
|
|
10
8
|
import readDescription from "../../prompts/tools/read.md" with { type: "text" };
|
|
11
9
|
import { formatDimensionNote, resizeImage } from "../../utils/image-resize";
|
|
@@ -13,7 +11,7 @@ import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime";
|
|
|
13
11
|
import { ensureTool } from "../../utils/tools-manager";
|
|
14
12
|
import type { RenderResultOptions } from "../custom-tools/types";
|
|
15
13
|
import type { ToolSession } from "../sdk";
|
|
16
|
-
import { untilAborted } from "../utils";
|
|
14
|
+
import { ScopeSignal, untilAborted } from "../utils";
|
|
17
15
|
import { createLsTool } from "./ls";
|
|
18
16
|
import { resolveReadPath, resolveToCwd } from "./path-utils";
|
|
19
17
|
import { replaceTabs, shortenPath, wrapBrackets } from "./render-utils";
|
|
@@ -55,20 +53,16 @@ function isPathWithin(basePath: string, targetPath: string): boolean {
|
|
|
55
53
|
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
56
54
|
}
|
|
57
55
|
|
|
58
|
-
async function findExistingDirectory(startDir: string): Promise<string | null> {
|
|
56
|
+
async function findExistingDirectory(startDir: string, signal?: AbortSignal): Promise<string | null> {
|
|
59
57
|
let current = startDir;
|
|
60
58
|
const root = path.parse(startDir).root;
|
|
61
59
|
|
|
62
60
|
while (true) {
|
|
61
|
+
signal?.throwIfAborted();
|
|
63
62
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
await Bun.$`test -d ${current}`.quiet();
|
|
68
|
-
return current;
|
|
69
|
-
} catch {
|
|
70
|
-
// Not a directory, continue
|
|
71
|
-
}
|
|
63
|
+
const stat = await Bun.file(current).stat();
|
|
64
|
+
if (stat.isDirectory()) {
|
|
65
|
+
return current;
|
|
72
66
|
}
|
|
73
67
|
} catch {
|
|
74
68
|
// Keep walking up.
|
|
@@ -149,8 +143,56 @@ function similarityScore(a: string, b: string): number {
|
|
|
149
143
|
return 1 - distance / maxLen;
|
|
150
144
|
}
|
|
151
145
|
|
|
146
|
+
async function captureCommandOutput(
|
|
147
|
+
command: string,
|
|
148
|
+
args: string[],
|
|
149
|
+
signal?: AbortSignal,
|
|
150
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number | null; aborted: boolean }> {
|
|
151
|
+
const child = Bun.spawn([command, ...args], {
|
|
152
|
+
stdin: "ignore",
|
|
153
|
+
stdout: "pipe",
|
|
154
|
+
stderr: "pipe",
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
using scope = new ScopeSignal(signal ? { signal } : undefined);
|
|
158
|
+
scope.catch(() => {
|
|
159
|
+
child.kill();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const stdoutReader = (child.stdout as ReadableStream<Uint8Array>).getReader();
|
|
163
|
+
const stderrReader = (child.stderr as ReadableStream<Uint8Array>).getReader();
|
|
164
|
+
const stdoutDecoder = new TextDecoder();
|
|
165
|
+
const stderrDecoder = new TextDecoder();
|
|
166
|
+
let stdout = "";
|
|
167
|
+
let stderr = "";
|
|
168
|
+
|
|
169
|
+
await Promise.all([
|
|
170
|
+
(async () => {
|
|
171
|
+
while (true) {
|
|
172
|
+
const { done, value } = await stdoutReader.read();
|
|
173
|
+
if (done) break;
|
|
174
|
+
stdout += stdoutDecoder.decode(value, { stream: true });
|
|
175
|
+
}
|
|
176
|
+
stdout += stdoutDecoder.decode();
|
|
177
|
+
})(),
|
|
178
|
+
(async () => {
|
|
179
|
+
while (true) {
|
|
180
|
+
const { done, value } = await stderrReader.read();
|
|
181
|
+
if (done) break;
|
|
182
|
+
stderr += stderrDecoder.decode(value, { stream: true });
|
|
183
|
+
}
|
|
184
|
+
stderr += stderrDecoder.decode();
|
|
185
|
+
})(),
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
const exitCode = await child.exited;
|
|
189
|
+
|
|
190
|
+
return { stdout, stderr, exitCode, aborted: scope.aborted };
|
|
191
|
+
}
|
|
192
|
+
|
|
152
193
|
async function listCandidateFiles(
|
|
153
194
|
searchRoot: string,
|
|
195
|
+
signal?: AbortSignal,
|
|
154
196
|
): Promise<{ files: string[]; truncated: boolean; error?: string }> {
|
|
155
197
|
let fdPath: string | undefined;
|
|
156
198
|
try {
|
|
@@ -167,22 +209,48 @@ async function listCandidateFiles(
|
|
|
167
209
|
|
|
168
210
|
const gitignoreFiles = new Set<string>();
|
|
169
211
|
const rootGitignore = path.join(searchRoot, ".gitignore");
|
|
170
|
-
if (
|
|
212
|
+
if (await Bun.file(rootGitignore).exists()) {
|
|
171
213
|
gitignoreFiles.add(rootGitignore);
|
|
172
214
|
}
|
|
173
215
|
|
|
174
216
|
try {
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
217
|
+
const gitignoreArgs = [
|
|
218
|
+
"--type",
|
|
219
|
+
"f",
|
|
220
|
+
"--color=never",
|
|
221
|
+
"--hidden",
|
|
222
|
+
"--absolute-path",
|
|
223
|
+
"--glob",
|
|
224
|
+
".gitignore",
|
|
225
|
+
"--exclude",
|
|
226
|
+
"node_modules",
|
|
227
|
+
"--exclude",
|
|
228
|
+
".git",
|
|
229
|
+
searchRoot,
|
|
230
|
+
];
|
|
231
|
+
const { stdout, aborted } = await captureCommandOutput(fdPath, gitignoreArgs, signal);
|
|
232
|
+
if (aborted) {
|
|
233
|
+
throw new Error("Operation aborted");
|
|
183
234
|
}
|
|
184
|
-
|
|
185
|
-
|
|
235
|
+
const output = stdout.trim();
|
|
236
|
+
if (output) {
|
|
237
|
+
const nestedGitignores = output
|
|
238
|
+
.split("\n")
|
|
239
|
+
.map((line) => line.replace(/\r$/, "").trim())
|
|
240
|
+
.filter((line) => line.length > 0);
|
|
241
|
+
for (const file of nestedGitignores) {
|
|
242
|
+
const normalized = file.replace(/\\/g, "/");
|
|
243
|
+
if (normalized.includes("/node_modules/") || normalized.includes("/.git/")) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
gitignoreFiles.add(file);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (error instanceof Error && error.message === "Operation aborted") {
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
// Ignore gitignore scan errors.
|
|
186
254
|
}
|
|
187
255
|
|
|
188
256
|
for (const gitignorePath of gitignoreFiles) {
|
|
@@ -191,16 +259,16 @@ async function listCandidateFiles(
|
|
|
191
259
|
|
|
192
260
|
args.push(".", searchRoot);
|
|
193
261
|
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
262
|
+
const { stdout, stderr, exitCode, aborted } = await captureCommandOutput(fdPath, args, signal);
|
|
263
|
+
|
|
264
|
+
if (aborted) {
|
|
265
|
+
throw new Error("Operation aborted");
|
|
266
|
+
}
|
|
199
267
|
|
|
200
|
-
const output =
|
|
268
|
+
const output = stdout.trim();
|
|
201
269
|
|
|
202
|
-
if (
|
|
203
|
-
const errorMsg =
|
|
270
|
+
if (exitCode !== 0 && !output) {
|
|
271
|
+
const errorMsg = stderr.trim() || `fd exited with code ${exitCode ?? -1}`;
|
|
204
272
|
return { files: [], truncated: false, error: errorMsg };
|
|
205
273
|
}
|
|
206
274
|
|
|
@@ -219,9 +287,10 @@ async function listCandidateFiles(
|
|
|
219
287
|
async function findReadPathSuggestions(
|
|
220
288
|
rawPath: string,
|
|
221
289
|
cwd: string,
|
|
290
|
+
signal?: AbortSignal,
|
|
222
291
|
): Promise<{ suggestions: string[]; scopeLabel?: string; truncated?: boolean; error?: string } | null> {
|
|
223
292
|
const resolvedPath = resolveToCwd(rawPath, cwd);
|
|
224
|
-
const searchRoot = await findExistingDirectory(path.dirname(resolvedPath));
|
|
293
|
+
const searchRoot = await findExistingDirectory(path.dirname(resolvedPath), signal);
|
|
225
294
|
if (!searchRoot) {
|
|
226
295
|
return null;
|
|
227
296
|
}
|
|
@@ -233,7 +302,7 @@ async function findReadPathSuggestions(
|
|
|
233
302
|
}
|
|
234
303
|
}
|
|
235
304
|
|
|
236
|
-
const { files, truncated, error } = await listCandidateFiles(searchRoot);
|
|
305
|
+
const { files, truncated, error } = await listCandidateFiles(searchRoot, signal);
|
|
237
306
|
const scopeLabel = formatScopeLabel(searchRoot, cwd);
|
|
238
307
|
|
|
239
308
|
if (error && files.length === 0) {
|
|
@@ -259,6 +328,7 @@ async function findReadPathSuggestions(
|
|
|
259
328
|
const seen = new Set<string>();
|
|
260
329
|
|
|
261
330
|
for (const file of files) {
|
|
331
|
+
signal?.throwIfAborted();
|
|
262
332
|
const cleaned = file.replace(/\r$/, "").trim();
|
|
263
333
|
if (!cleaned) continue;
|
|
264
334
|
|
|
@@ -311,23 +381,26 @@ async function findReadPathSuggestions(
|
|
|
311
381
|
return { suggestions, scopeLabel, truncated };
|
|
312
382
|
}
|
|
313
383
|
|
|
314
|
-
async function convertWithMarkitdown(
|
|
384
|
+
async function convertWithMarkitdown(
|
|
385
|
+
filePath: string,
|
|
386
|
+
signal?: AbortSignal,
|
|
387
|
+
): Promise<{ content: string; ok: boolean; error?: string }> {
|
|
315
388
|
const cmd = await ensureTool("markitdown", true);
|
|
316
389
|
if (!cmd) {
|
|
317
390
|
return { content: "", ok: false, error: "markitdown not found (uv/pip unavailable)" };
|
|
318
391
|
}
|
|
319
392
|
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
393
|
+
const { stdout, stderr, exitCode, aborted } = await captureCommandOutput(cmd, [filePath], signal);
|
|
394
|
+
|
|
395
|
+
if (aborted) {
|
|
396
|
+
throw new Error("Operation aborted");
|
|
397
|
+
}
|
|
325
398
|
|
|
326
|
-
if (
|
|
327
|
-
return { content:
|
|
399
|
+
if (exitCode === 0 && stdout.length > 0) {
|
|
400
|
+
return { content: stdout, ok: true };
|
|
328
401
|
}
|
|
329
402
|
|
|
330
|
-
return { content: "", ok: false, error:
|
|
403
|
+
return { content: "", ok: false, error: stderr.trim() || "Conversion failed" };
|
|
331
404
|
}
|
|
332
405
|
|
|
333
406
|
const readSchema = Type.Object({
|
|
@@ -360,21 +433,12 @@ export function createReadTool(session: ToolSession): AgentTool<typeof readSchem
|
|
|
360
433
|
let isDirectory = false;
|
|
361
434
|
let fileSize = 0;
|
|
362
435
|
try {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const file = Bun.file(absolutePath);
|
|
367
|
-
fileSize = file.size;
|
|
368
|
-
// Check if directory
|
|
369
|
-
try {
|
|
370
|
-
await Bun.$`test -d ${absolutePath}`.quiet();
|
|
371
|
-
isDirectory = true;
|
|
372
|
-
} catch {
|
|
373
|
-
isDirectory = false;
|
|
374
|
-
}
|
|
436
|
+
const stat = await Bun.file(absolutePath).stat();
|
|
437
|
+
fileSize = stat.size;
|
|
438
|
+
isDirectory = stat.isDirectory();
|
|
375
439
|
} catch (error) {
|
|
376
440
|
if (isNotFoundError(error)) {
|
|
377
|
-
const suggestions = await findReadPathSuggestions(readPath, session.cwd);
|
|
441
|
+
const suggestions = await findReadPathSuggestions(readPath, session.cwd, signal);
|
|
378
442
|
let message = `File not found: ${readPath}`;
|
|
379
443
|
|
|
380
444
|
if (suggestions?.suggestions.length) {
|
|
@@ -410,7 +474,6 @@ export function createReadTool(session: ToolSession): AgentTool<typeof readSchem
|
|
|
410
474
|
let details: ReadToolDetails | undefined;
|
|
411
475
|
|
|
412
476
|
if (mimeType) {
|
|
413
|
-
// Check image file size before reading to prevent OOM during serialization
|
|
414
477
|
if (fileSize > MAX_IMAGE_SIZE) {
|
|
415
478
|
const sizeStr = formatSize(fileSize);
|
|
416
479
|
const maxStr = formatSize(MAX_IMAGE_SIZE);
|
|
@@ -424,32 +487,45 @@ export function createReadTool(session: ToolSession): AgentTool<typeof readSchem
|
|
|
424
487
|
// Read as image (binary)
|
|
425
488
|
const file = Bun.file(absolutePath);
|
|
426
489
|
const buffer = await file.arrayBuffer();
|
|
427
|
-
const base64 = Buffer.from(buffer).toString("base64");
|
|
428
|
-
|
|
429
|
-
if (autoResizeImages) {
|
|
430
|
-
// Resize image if needed
|
|
431
|
-
const resized = await resizeImage({ type: "image", data: base64, mimeType });
|
|
432
|
-
const dimensionNote = formatDimensionNote(resized);
|
|
433
|
-
|
|
434
|
-
let textNote = `Read image file [${resized.mimeType}]`;
|
|
435
|
-
if (dimensionNote) {
|
|
436
|
-
textNote += `\n${dimensionNote}`;
|
|
437
|
-
}
|
|
438
490
|
|
|
491
|
+
// Check actual buffer size after reading to prevent OOM during serialization
|
|
492
|
+
if (buffer.byteLength > MAX_IMAGE_SIZE) {
|
|
493
|
+
const sizeStr = formatSize(buffer.byteLength);
|
|
494
|
+
const maxStr = formatSize(MAX_IMAGE_SIZE);
|
|
439
495
|
content = [
|
|
440
|
-
{
|
|
441
|
-
|
|
496
|
+
{
|
|
497
|
+
type: "text",
|
|
498
|
+
text: `[Image file too large: ${sizeStr} exceeds ${maxStr} limit. Use an image viewer or resize the image.]`,
|
|
499
|
+
},
|
|
442
500
|
];
|
|
443
501
|
} else {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
502
|
+
const base64 = Buffer.from(buffer).toString("base64");
|
|
503
|
+
|
|
504
|
+
if (autoResizeImages) {
|
|
505
|
+
// Resize image if needed
|
|
506
|
+
const resized = await resizeImage({ type: "image", data: base64, mimeType });
|
|
507
|
+
const dimensionNote = formatDimensionNote(resized);
|
|
508
|
+
|
|
509
|
+
let textNote = `Read image file [${resized.mimeType}]`;
|
|
510
|
+
if (dimensionNote) {
|
|
511
|
+
textNote += `\n${dimensionNote}`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
content = [
|
|
515
|
+
{ type: "text", text: textNote },
|
|
516
|
+
{ type: "image", data: resized.data, mimeType: resized.mimeType },
|
|
517
|
+
];
|
|
518
|
+
} else {
|
|
519
|
+
content = [
|
|
520
|
+
{ type: "text", text: `Read image file [${mimeType}]` },
|
|
521
|
+
{ type: "image", data: base64, mimeType },
|
|
522
|
+
];
|
|
523
|
+
}
|
|
448
524
|
}
|
|
449
525
|
}
|
|
450
526
|
} else if (CONVERTIBLE_EXTENSIONS.has(ext)) {
|
|
451
527
|
// Convert document via markitdown
|
|
452
|
-
const result = await convertWithMarkitdown(absolutePath);
|
|
528
|
+
const result = await convertWithMarkitdown(absolutePath, signal);
|
|
453
529
|
if (result.ok) {
|
|
454
530
|
// Apply truncation to converted content
|
|
455
531
|
const truncation = truncateHead(result.content);
|
|
@@ -147,11 +147,7 @@ export function formatAge(ageSeconds: number | null | undefined): string {
|
|
|
147
147
|
* Get the appropriate status icon with color for a given state.
|
|
148
148
|
* Standardizes status icon usage across all renderers.
|
|
149
149
|
*/
|
|
150
|
-
export function getStyledStatusIcon(
|
|
151
|
-
status: "success" | "error" | "warning" | "info" | "pending" | "running" | "aborted",
|
|
152
|
-
theme: Theme,
|
|
153
|
-
spinnerFrame?: number,
|
|
154
|
-
): string {
|
|
150
|
+
export function getStyledStatusIcon(status: ToolUIStatus, theme: Theme, spinnerFrame?: number): string {
|
|
155
151
|
switch (status) {
|
|
156
152
|
case "success":
|
|
157
153
|
return theme.styledSymbol("status.success", "success");
|
|
@@ -185,11 +181,7 @@ export function formatExpandHint(expanded: boolean, hasMore: boolean, theme: The
|
|
|
185
181
|
/**
|
|
186
182
|
* Format a badge like [done] or [failed] with brackets and color.
|
|
187
183
|
*/
|
|
188
|
-
export function formatBadge(
|
|
189
|
-
label: string,
|
|
190
|
-
color: "success" | "error" | "warning" | "accent" | "muted",
|
|
191
|
-
theme: Theme,
|
|
192
|
-
): string {
|
|
184
|
+
export function formatBadge(label: string, color: ToolUIColor, theme: Theme): string {
|
|
193
185
|
const left = theme.format.bracketLeft;
|
|
194
186
|
const right = theme.format.bracketRight;
|
|
195
187
|
return theme.fg(color, `${left}${label}${right}`);
|
|
@@ -225,6 +217,74 @@ export function formatEmptyMessage(message: string, theme: Theme): string {
|
|
|
225
217
|
return `${theme.styledSymbol("status.warning", "warning")} ${theme.fg("muted", message)}`;
|
|
226
218
|
}
|
|
227
219
|
|
|
220
|
+
// =============================================================================
|
|
221
|
+
// Tool UI Kit
|
|
222
|
+
// =============================================================================
|
|
223
|
+
|
|
224
|
+
export type ToolUIStatus = "success" | "error" | "warning" | "info" | "pending" | "running" | "aborted";
|
|
225
|
+
export type ToolUIColor = "success" | "error" | "warning" | "accent" | "muted";
|
|
226
|
+
|
|
227
|
+
export interface ToolUITitleOptions {
|
|
228
|
+
bold?: boolean;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface ToolUIKit {
|
|
232
|
+
theme: Theme;
|
|
233
|
+
title: (label: string, options?: ToolUITitleOptions) => string;
|
|
234
|
+
meta: (meta: string[]) => string;
|
|
235
|
+
count: (label: string, count: number) => string;
|
|
236
|
+
moreItems: (remaining: number, itemType: string) => string;
|
|
237
|
+
expandHint: (expanded: boolean, hasMore: boolean) => string;
|
|
238
|
+
scope: (scopePath?: string) => string;
|
|
239
|
+
truncationSuffix: (truncated: boolean) => string;
|
|
240
|
+
errorMessage: (message: string | undefined) => string;
|
|
241
|
+
emptyMessage: (message: string) => string;
|
|
242
|
+
badge: (label: string, color: ToolUIColor) => string;
|
|
243
|
+
statusIcon: (status: ToolUIStatus, spinnerFrame?: number) => string;
|
|
244
|
+
wrapBrackets: (text: string) => string;
|
|
245
|
+
truncate: (text: string, maxLen: number) => string;
|
|
246
|
+
previewLines: (text: string, maxLines: number, maxLineLen: number) => string[];
|
|
247
|
+
formatBytes: (bytes: number) => string;
|
|
248
|
+
formatTokens: (tokens: number) => string;
|
|
249
|
+
formatDuration: (ms: number) => string;
|
|
250
|
+
formatAge: (ageSeconds: number | null | undefined) => string;
|
|
251
|
+
formatDiagnostics: (
|
|
252
|
+
diag: { errored: boolean; summary: string; messages: string[] },
|
|
253
|
+
expanded: boolean,
|
|
254
|
+
getLangIcon: (filePath: string) => string,
|
|
255
|
+
) => string;
|
|
256
|
+
formatDiffStats: (added: number, removed: number, hunks: number) => string;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function createToolUIKit(theme: Theme): ToolUIKit {
|
|
260
|
+
return {
|
|
261
|
+
theme,
|
|
262
|
+
title: (label, options) => {
|
|
263
|
+
const content = options?.bold === false ? label : theme.bold(label);
|
|
264
|
+
return theme.fg("toolTitle", content);
|
|
265
|
+
},
|
|
266
|
+
meta: (meta) => formatMeta(meta, theme),
|
|
267
|
+
count: (label, count) => formatCount(label, count),
|
|
268
|
+
moreItems: (remaining, itemType) => formatMoreItems(remaining, itemType, theme),
|
|
269
|
+
expandHint: (expanded, hasMore) => formatExpandHint(expanded, hasMore, theme),
|
|
270
|
+
scope: (scopePath) => formatScope(scopePath, theme),
|
|
271
|
+
truncationSuffix: (truncated) => formatTruncationSuffix(truncated, theme),
|
|
272
|
+
errorMessage: (message) => formatErrorMessage(message, theme),
|
|
273
|
+
emptyMessage: (message) => formatEmptyMessage(message, theme),
|
|
274
|
+
badge: (label, color) => formatBadge(label, color, theme),
|
|
275
|
+
statusIcon: (status, spinnerFrame) => getStyledStatusIcon(status, theme, spinnerFrame),
|
|
276
|
+
wrapBrackets: (text) => wrapBrackets(text, theme),
|
|
277
|
+
truncate: (text, maxLen) => truncate(text, maxLen, theme.format.ellipsis),
|
|
278
|
+
previewLines: (text, maxLines, maxLineLen) => getPreviewLines(text, maxLines, maxLineLen, theme.format.ellipsis),
|
|
279
|
+
formatBytes,
|
|
280
|
+
formatTokens,
|
|
281
|
+
formatDuration,
|
|
282
|
+
formatAge,
|
|
283
|
+
formatDiagnostics: (diag, expanded, getLangIcon) => formatDiagnostics(diag, expanded, theme, getLangIcon),
|
|
284
|
+
formatDiffStats: (added, removed, hunks) => formatDiffStats(added, removed, hunks, theme),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
228
288
|
// =============================================================================
|
|
229
289
|
// Diagnostic Formatting
|
|
230
290
|
// =============================================================================
|