@oh-my-pi/pi-coding-agent 3.25.0 → 3.31.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 +90 -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 +369 -14
- package/src/core/custom-commands/bundled/wt/index.ts +1 -1
- 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/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/complete.ts +2 -4
- 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 +6 -8
- package/src/core/tools/jtd-to-json-schema.ts +174 -196
- package/src/core/tools/ls.ts +12 -10
- package/src/core/tools/lsp/client.ts +58 -9
- 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 +55 -32
- 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 +152 -76
- 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/executor.ts +204 -67
- package/src/core/tools/task/index.ts +129 -92
- package/src/core/tools/task/name-generator.ts +1544 -214
- 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 +34 -11
- package/src/core/tools/task/worker.ts +152 -27
- package/src/core/tools/web-fetch.ts +220 -1657
- package/src/core/tools/web-scrapers/academic.test.ts +239 -0
- package/src/core/tools/web-scrapers/artifacthub.ts +215 -0
- package/src/core/tools/web-scrapers/arxiv.ts +88 -0
- package/src/core/tools/web-scrapers/aur.ts +175 -0
- package/src/core/tools/web-scrapers/biorxiv.ts +141 -0
- package/src/core/tools/web-scrapers/bluesky.ts +284 -0
- package/src/core/tools/web-scrapers/brew.ts +177 -0
- package/src/core/tools/web-scrapers/business.test.ts +82 -0
- package/src/core/tools/web-scrapers/cheatsh.ts +78 -0
- package/src/core/tools/web-scrapers/chocolatey.ts +158 -0
- 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-scrapers/coingecko.ts +184 -0
- package/src/core/tools/web-scrapers/crates-io.ts +128 -0
- package/src/core/tools/web-scrapers/crossref.ts +149 -0
- package/src/core/tools/web-scrapers/dev-platforms.test.ts +254 -0
- package/src/core/tools/web-scrapers/devto.ts +177 -0
- package/src/core/tools/web-scrapers/discogs.ts +308 -0
- package/src/core/tools/web-scrapers/discourse.ts +221 -0
- package/src/core/tools/web-scrapers/dockerhub.ts +160 -0
- package/src/core/tools/web-scrapers/documentation.test.ts +85 -0
- package/src/core/tools/web-scrapers/fdroid.ts +158 -0
- package/src/core/tools/web-scrapers/finance-media.test.ts +144 -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-scrapers/git-hosting.test.ts +272 -0
- package/src/core/tools/web-scrapers/github-gist.ts +68 -0
- package/src/core/tools/web-scrapers/github.ts +455 -0
- package/src/core/tools/web-scrapers/gitlab.ts +456 -0
- package/src/core/tools/web-scrapers/go-pkg.ts +275 -0
- package/src/core/tools/web-scrapers/hackage.ts +94 -0
- package/src/core/tools/web-scrapers/hackernews.ts +208 -0
- package/src/core/tools/web-scrapers/hex.ts +121 -0
- package/src/core/tools/web-scrapers/huggingface.ts +385 -0
- package/src/core/tools/web-scrapers/iacr.ts +86 -0
- 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-scrapers/lobsters.ts +186 -0
- package/src/core/tools/web-scrapers/mastodon.ts +310 -0
- package/src/core/tools/web-scrapers/maven.ts +152 -0
- package/src/core/tools/web-scrapers/mdn.ts +174 -0
- package/src/core/tools/web-scrapers/media.test.ts +138 -0
- package/src/core/tools/web-scrapers/metacpan.ts +253 -0
- package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
- package/src/core/tools/web-scrapers/npm.ts +114 -0
- package/src/core/tools/web-scrapers/nuget.ts +205 -0
- package/src/core/tools/web-scrapers/nvd.ts +243 -0
- 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-scrapers/opencorporates.ts +275 -0
- package/src/core/tools/web-scrapers/openlibrary.ts +319 -0
- package/src/core/tools/web-scrapers/orcid.ts +299 -0
- package/src/core/tools/web-scrapers/osv.ts +189 -0
- package/src/core/tools/web-scrapers/package-managers-2.test.ts +199 -0
- package/src/core/tools/web-scrapers/package-managers.test.ts +171 -0
- package/src/core/tools/web-scrapers/package-registries.test.ts +259 -0
- package/src/core/tools/web-scrapers/packagist.ts +174 -0
- package/src/core/tools/web-scrapers/pub-dev.ts +185 -0
- package/src/core/tools/web-scrapers/pubmed.ts +178 -0
- package/src/core/tools/web-scrapers/pypi.ts +129 -0
- package/src/core/tools/web-scrapers/rawg.ts +124 -0
- package/src/core/tools/web-scrapers/readthedocs.ts +126 -0
- package/src/core/tools/web-scrapers/reddit.ts +104 -0
- package/src/core/tools/web-scrapers/repology.ts +262 -0
- package/src/core/tools/web-scrapers/research.test.ts +107 -0
- package/src/core/tools/web-scrapers/rfc.ts +209 -0
- package/src/core/tools/web-scrapers/rubygems.ts +117 -0
- package/src/core/tools/web-scrapers/searchcode.ts +217 -0
- package/src/core/tools/web-scrapers/sec-edgar.ts +274 -0
- package/src/core/tools/web-scrapers/security.test.ts +103 -0
- package/src/core/tools/web-scrapers/semantic-scholar.ts +190 -0
- package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
- package/src/core/tools/web-scrapers/social-extended.test.ts +192 -0
- package/src/core/tools/web-scrapers/social.test.ts +259 -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-scrapers/spotify.ts +218 -0
- package/src/core/tools/web-scrapers/stackexchange.test.ts +120 -0
- package/src/core/tools/web-scrapers/stackoverflow.ts +124 -0
- package/src/core/tools/web-scrapers/standards.test.ts +122 -0
- package/src/core/tools/web-scrapers/terraform.ts +304 -0
- package/src/core/tools/web-scrapers/tldr.ts +51 -0
- package/src/core/tools/web-scrapers/twitter.ts +96 -0
- package/src/core/tools/web-scrapers/types.ts +234 -0
- package/src/core/tools/web-scrapers/utils.ts +162 -0
- package/src/core/tools/web-scrapers/vimeo.ts +152 -0
- 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-scrapers/wikidata.ts +357 -0
- package/src/core/tools/web-scrapers/wikipedia.test.ts +73 -0
- package/src/core/tools/web-scrapers/wikipedia.ts +95 -0
- package/src/core/tools/web-scrapers/youtube.test.ts +198 -0
- package/src/core/tools/web-scrapers/youtube.ts +371 -0
- 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 -38
- 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/src/utils/tools-manager.ts +110 -8
- package/examples/extensions/subagent/agents/reviewer.md +0 -35
package/src/core/tools/find.ts
CHANGED
|
@@ -1,28 +1,16 @@
|
|
|
1
|
-
import { existsSync, type Stats, statSync } from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
4
3
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
5
4
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
6
5
|
import { Type } from "@sinclair/typebox";
|
|
7
|
-
import { globSync } from "glob";
|
|
8
6
|
import { getLanguageFromPath, type Theme } from "../../modes/interactive/theme/theme";
|
|
9
7
|
import findDescription from "../../prompts/tools/find.md" with { type: "text" };
|
|
10
8
|
import { ensureTool } from "../../utils/tools-manager";
|
|
11
9
|
import type { RenderResultOptions } from "../custom-tools/types";
|
|
12
|
-
import { untilAborted } from "../utils";
|
|
10
|
+
import { ScopeSignal, untilAborted } from "../utils";
|
|
13
11
|
import type { ToolSession } from "./index";
|
|
14
12
|
import { resolveToCwd } from "./path-utils";
|
|
15
|
-
import {
|
|
16
|
-
formatCount,
|
|
17
|
-
formatEmptyMessage,
|
|
18
|
-
formatErrorMessage,
|
|
19
|
-
formatExpandHint,
|
|
20
|
-
formatMeta,
|
|
21
|
-
formatMoreItems,
|
|
22
|
-
formatScope,
|
|
23
|
-
formatTruncationSuffix,
|
|
24
|
-
PREVIEW_LIMITS,
|
|
25
|
-
} from "./render-utils";
|
|
13
|
+
import { createToolUIKit, PREVIEW_LIMITS } from "./render-utils";
|
|
26
14
|
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate";
|
|
27
15
|
|
|
28
16
|
const findSchema = Type.Object({
|
|
@@ -56,6 +44,53 @@ export interface FindToolDetails {
|
|
|
56
44
|
error?: string;
|
|
57
45
|
}
|
|
58
46
|
|
|
47
|
+
async function captureCommandOutput(
|
|
48
|
+
command: string,
|
|
49
|
+
args: string[],
|
|
50
|
+
signal?: AbortSignal,
|
|
51
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number | null; aborted: boolean }> {
|
|
52
|
+
const child = Bun.spawn([command, ...args], {
|
|
53
|
+
stdin: "ignore",
|
|
54
|
+
stdout: "pipe",
|
|
55
|
+
stderr: "pipe",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
using scope = new ScopeSignal(signal ? { signal } : undefined);
|
|
59
|
+
scope.catch(() => {
|
|
60
|
+
child.kill();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const stdoutReader = (child.stdout as ReadableStream<Uint8Array>).getReader();
|
|
64
|
+
const stderrReader = (child.stderr as ReadableStream<Uint8Array>).getReader();
|
|
65
|
+
const stdoutDecoder = new TextDecoder();
|
|
66
|
+
const stderrDecoder = new TextDecoder();
|
|
67
|
+
let stdout = "";
|
|
68
|
+
let stderr = "";
|
|
69
|
+
|
|
70
|
+
await Promise.all([
|
|
71
|
+
(async () => {
|
|
72
|
+
while (true) {
|
|
73
|
+
const { done, value } = await stdoutReader.read();
|
|
74
|
+
if (done) break;
|
|
75
|
+
stdout += stdoutDecoder.decode(value, { stream: true });
|
|
76
|
+
}
|
|
77
|
+
stdout += stdoutDecoder.decode();
|
|
78
|
+
})(),
|
|
79
|
+
(async () => {
|
|
80
|
+
while (true) {
|
|
81
|
+
const { done, value } = await stderrReader.read();
|
|
82
|
+
if (done) break;
|
|
83
|
+
stderr += stderrDecoder.decode(value, { stream: true });
|
|
84
|
+
}
|
|
85
|
+
stderr += stderrDecoder.decode();
|
|
86
|
+
})(),
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
const exitCode = await child.exited;
|
|
90
|
+
|
|
91
|
+
return { stdout, stderr, exitCode, aborted: scope.aborted };
|
|
92
|
+
}
|
|
93
|
+
|
|
59
94
|
export function createFindTool(session: ToolSession): AgentTool<typeof findSchema> {
|
|
60
95
|
return {
|
|
61
96
|
name: "find",
|
|
@@ -126,22 +161,43 @@ export function createFindTool(session: ToolSession): AgentTool<typeof findSchem
|
|
|
126
161
|
// Include .gitignore files (root + nested) so fd respects them even outside git repos
|
|
127
162
|
const gitignoreFiles = new Set<string>();
|
|
128
163
|
const rootGitignore = path.join(searchPath, ".gitignore");
|
|
129
|
-
if (
|
|
164
|
+
if (await Bun.file(rootGitignore).exists()) {
|
|
130
165
|
gitignoreFiles.add(rootGitignore);
|
|
131
166
|
}
|
|
132
167
|
|
|
133
168
|
try {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
169
|
+
const gitignoreArgs = [
|
|
170
|
+
"--hidden",
|
|
171
|
+
"--no-ignore",
|
|
172
|
+
"--type",
|
|
173
|
+
"f",
|
|
174
|
+
"--name",
|
|
175
|
+
".gitignore",
|
|
176
|
+
"--exclude",
|
|
177
|
+
".git",
|
|
178
|
+
"--exclude",
|
|
179
|
+
"node_modules",
|
|
180
|
+
"--absolute-path",
|
|
181
|
+
searchPath,
|
|
182
|
+
];
|
|
183
|
+
const { stdout: gitignoreStdout, aborted: gitignoreAborted } = await captureCommandOutput(
|
|
184
|
+
fdPath,
|
|
185
|
+
gitignoreArgs,
|
|
186
|
+
signal,
|
|
187
|
+
);
|
|
188
|
+
if (gitignoreAborted) {
|
|
189
|
+
throw new Error("Operation aborted");
|
|
190
|
+
}
|
|
191
|
+
for (const rawLine of gitignoreStdout.split("\n")) {
|
|
192
|
+
const file = rawLine.trim();
|
|
193
|
+
if (!file) continue;
|
|
141
194
|
gitignoreFiles.add(file);
|
|
142
195
|
}
|
|
143
|
-
} catch {
|
|
144
|
-
|
|
196
|
+
} catch (err) {
|
|
197
|
+
if (signal?.aborted) {
|
|
198
|
+
throw err instanceof Error ? err : new Error("Operation aborted");
|
|
199
|
+
}
|
|
200
|
+
// Ignore lookup errors
|
|
145
201
|
}
|
|
146
202
|
|
|
147
203
|
for (const gitignorePath of gitignoreFiles) {
|
|
@@ -152,16 +208,16 @@ export function createFindTool(session: ToolSession): AgentTool<typeof findSchem
|
|
|
152
208
|
args.push(effectivePattern, searchPath);
|
|
153
209
|
|
|
154
210
|
// Run fd
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
211
|
+
const { stdout, stderr, exitCode, aborted } = await captureCommandOutput(fdPath, args, signal);
|
|
212
|
+
|
|
213
|
+
if (aborted) {
|
|
214
|
+
throw new Error("Operation aborted");
|
|
215
|
+
}
|
|
160
216
|
|
|
161
|
-
const output =
|
|
217
|
+
const output = stdout.trim();
|
|
162
218
|
|
|
163
|
-
if (
|
|
164
|
-
const errorMsg =
|
|
219
|
+
if (exitCode !== 0) {
|
|
220
|
+
const errorMsg = stderr.trim() || `fd exited with code ${exitCode ?? -1}`;
|
|
165
221
|
// fd returns non-zero for some errors but may still have partial output
|
|
166
222
|
if (!output) {
|
|
167
223
|
throw new Error(errorMsg);
|
|
@@ -180,6 +236,7 @@ export function createFindTool(session: ToolSession): AgentTool<typeof findSchem
|
|
|
180
236
|
const mtimes: number[] = [];
|
|
181
237
|
|
|
182
238
|
for (const rawLine of lines) {
|
|
239
|
+
signal?.throwIfAborted();
|
|
183
240
|
const line = rawLine.replace(/\r$/, "").trim();
|
|
184
241
|
if (!line) {
|
|
185
242
|
continue;
|
|
@@ -197,23 +254,25 @@ export function createFindTool(session: ToolSession): AgentTool<typeof findSchem
|
|
|
197
254
|
relativePath += "/";
|
|
198
255
|
}
|
|
199
256
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
// Collect mtime if sorting is requested
|
|
257
|
+
// When sorting by mtime, keep files that fail to stat with mtime 0
|
|
203
258
|
if (shouldSortByMtime) {
|
|
204
259
|
try {
|
|
205
260
|
const fullPath = path.join(searchPath, relativePath);
|
|
206
|
-
const stat
|
|
261
|
+
const stat = await Bun.file(fullPath).stat();
|
|
262
|
+
relativized.push(relativePath);
|
|
207
263
|
mtimes.push(stat.mtimeMs);
|
|
208
264
|
} catch {
|
|
265
|
+
relativized.push(relativePath);
|
|
209
266
|
mtimes.push(0);
|
|
210
267
|
}
|
|
268
|
+
} else {
|
|
269
|
+
relativized.push(relativePath);
|
|
211
270
|
}
|
|
212
271
|
}
|
|
213
272
|
|
|
214
273
|
// Sort by mtime if requested (most recent first)
|
|
215
274
|
if (shouldSortByMtime && relativized.length > 0) {
|
|
216
|
-
const indexed = relativized.map((path, idx) => ({ path, mtime: mtimes[idx]
|
|
275
|
+
const indexed = relativized.map((path, idx) => ({ path, mtime: mtimes[idx] }));
|
|
217
276
|
indexed.sort((a, b) => b.mtime - a.mtime);
|
|
218
277
|
relativized.length = 0;
|
|
219
278
|
relativized.push(...indexed.map((item) => item.path));
|
|
@@ -279,7 +338,8 @@ const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
|
279
338
|
|
|
280
339
|
export const findToolRenderer = {
|
|
281
340
|
renderCall(args: FindRenderArgs, uiTheme: Theme): Component {
|
|
282
|
-
const
|
|
341
|
+
const ui = createToolUIKit(uiTheme);
|
|
342
|
+
const label = ui.title("Find");
|
|
283
343
|
let text = `${label} ${uiTheme.fg("accent", args.pattern || "*")}`;
|
|
284
344
|
|
|
285
345
|
const meta: string[] = [];
|
|
@@ -289,7 +349,7 @@ export const findToolRenderer = {
|
|
|
289
349
|
if (args.sortByMtime) meta.push("sort:mtime");
|
|
290
350
|
if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
|
|
291
351
|
|
|
292
|
-
text +=
|
|
352
|
+
text += ui.meta(meta);
|
|
293
353
|
|
|
294
354
|
return new Text(text, 0, 0);
|
|
295
355
|
},
|
|
@@ -299,10 +359,11 @@ export const findToolRenderer = {
|
|
|
299
359
|
{ expanded }: RenderResultOptions,
|
|
300
360
|
uiTheme: Theme,
|
|
301
361
|
): Component {
|
|
362
|
+
const ui = createToolUIKit(uiTheme);
|
|
302
363
|
const details = result.details;
|
|
303
364
|
|
|
304
365
|
if (details?.error) {
|
|
305
|
-
return new Text(
|
|
366
|
+
return new Text(ui.errorMessage(details.error), 0, 0);
|
|
306
367
|
}
|
|
307
368
|
|
|
308
369
|
const hasDetailedData = details?.fileCount !== undefined;
|
|
@@ -310,7 +371,7 @@ export const findToolRenderer = {
|
|
|
310
371
|
|
|
311
372
|
if (!hasDetailedData) {
|
|
312
373
|
if (!textContent || textContent.includes("No files matching") || textContent.trim() === "") {
|
|
313
|
-
return new Text(
|
|
374
|
+
return new Text(ui.emptyMessage("No files found"), 0, 0);
|
|
314
375
|
}
|
|
315
376
|
|
|
316
377
|
const lines = textContent.split("\n").filter((l) => l.trim());
|
|
@@ -320,8 +381,8 @@ export const findToolRenderer = {
|
|
|
320
381
|
const hasMore = remaining > 0;
|
|
321
382
|
|
|
322
383
|
const icon = uiTheme.styledSymbol("status.success", "success");
|
|
323
|
-
const summary =
|
|
324
|
-
const expandHint =
|
|
384
|
+
const summary = ui.count("file", lines.length);
|
|
385
|
+
const expandHint = ui.expandHint(expanded, hasMore);
|
|
325
386
|
let text = `${icon} ${uiTheme.fg("dim", summary)}${expandHint}`;
|
|
326
387
|
|
|
327
388
|
for (let i = 0; i < displayLines.length; i++) {
|
|
@@ -330,7 +391,7 @@ export const findToolRenderer = {
|
|
|
330
391
|
text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("accent", displayLines[i])}`;
|
|
331
392
|
}
|
|
332
393
|
if (remaining > 0) {
|
|
333
|
-
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted",
|
|
394
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", ui.moreItems(remaining, "file"))}`;
|
|
334
395
|
}
|
|
335
396
|
return new Text(text, 0, 0);
|
|
336
397
|
}
|
|
@@ -340,17 +401,17 @@ export const findToolRenderer = {
|
|
|
340
401
|
const files = details?.files ?? [];
|
|
341
402
|
|
|
342
403
|
if (fileCount === 0) {
|
|
343
|
-
return new Text(
|
|
404
|
+
return new Text(ui.emptyMessage("No files found"), 0, 0);
|
|
344
405
|
}
|
|
345
406
|
|
|
346
407
|
const icon = uiTheme.styledSymbol("status.success", "success");
|
|
347
|
-
const summaryText =
|
|
348
|
-
const scopeLabel =
|
|
408
|
+
const summaryText = ui.count("file", fileCount);
|
|
409
|
+
const scopeLabel = ui.scope(details?.scopePath);
|
|
349
410
|
const maxFiles = expanded ? files.length : Math.min(files.length, COLLAPSED_LIST_LIMIT);
|
|
350
411
|
const hasMoreFiles = files.length > maxFiles;
|
|
351
|
-
const expandHint =
|
|
412
|
+
const expandHint = ui.expandHint(expanded, hasMoreFiles);
|
|
352
413
|
|
|
353
|
-
let text = `${icon} ${uiTheme.fg("dim", summaryText)}${
|
|
414
|
+
let text = `${icon} ${uiTheme.fg("dim", summaryText)}${ui.truncationSuffix(truncated)}${scopeLabel}${expandHint}`;
|
|
354
415
|
|
|
355
416
|
const truncationReasons: string[] = [];
|
|
356
417
|
if (details?.resultLimitReached) {
|
|
@@ -380,7 +441,7 @@ export const findToolRenderer = {
|
|
|
380
441
|
const moreFilesBranch = hasTruncation ? uiTheme.tree.branch : uiTheme.tree.last;
|
|
381
442
|
text += `\n ${uiTheme.fg("dim", moreFilesBranch)} ${uiTheme.fg(
|
|
382
443
|
"muted",
|
|
383
|
-
|
|
444
|
+
ui.moreItems(files.length - maxFiles, "file"),
|
|
384
445
|
)}`;
|
|
385
446
|
}
|
|
386
447
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import * as crypto from "node:crypto";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
1
|
import { tmpdir } from "node:os";
|
|
4
2
|
import { join } from "node:path";
|
|
5
3
|
import { type Static, Type } from "@sinclair/typebox";
|
|
4
|
+
import { nanoid } from "nanoid";
|
|
6
5
|
import geminiImageDescription from "../../prompts/tools/gemini-image.md" with { type: "text" };
|
|
7
6
|
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime";
|
|
8
7
|
import type { CustomTool } from "../custom-tools/types";
|
|
@@ -311,14 +310,18 @@ function getExtensionForMime(mimeType: string): string {
|
|
|
311
310
|
return map[mimeType] ?? "png";
|
|
312
311
|
}
|
|
313
312
|
|
|
314
|
-
function saveImageToTemp(image: InlineImageData): string {
|
|
313
|
+
async function saveImageToTemp(image: InlineImageData): Promise<string> {
|
|
315
314
|
const ext = getExtensionForMime(image.mimeType);
|
|
316
|
-
const filename = `omp-image-${
|
|
315
|
+
const filename = `omp-image-${nanoid()}.${ext}`;
|
|
317
316
|
const filepath = join(tmpdir(), filename);
|
|
318
|
-
|
|
317
|
+
await Bun.write(filepath, Buffer.from(image.data, "base64"));
|
|
319
318
|
return filepath;
|
|
320
319
|
}
|
|
321
320
|
|
|
321
|
+
async function saveImagesToTemp(images: InlineImageData[]): Promise<string[]> {
|
|
322
|
+
return Promise.all(images.map(saveImageToTemp));
|
|
323
|
+
}
|
|
324
|
+
|
|
322
325
|
function buildResponseSummary(model: string, imagePaths: string[], responseText: string | undefined): string {
|
|
323
326
|
const lines = [`Model: ${model}`, `Generated ${imagePaths.length} image(s):`];
|
|
324
327
|
for (const p of imagePaths) {
|
|
@@ -356,27 +359,9 @@ function combineParts(response: GeminiGenerateContentResponse): GeminiPart[] {
|
|
|
356
359
|
return parts;
|
|
357
360
|
}
|
|
358
361
|
|
|
359
|
-
function
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
): { controller: AbortController; cleanup: () => void } {
|
|
363
|
-
const controller = new AbortController();
|
|
364
|
-
const timeout = setTimeout(() => controller.abort(), timeoutSeconds * 1000);
|
|
365
|
-
|
|
366
|
-
let abortListener: (() => void) | undefined;
|
|
367
|
-
if (signal) {
|
|
368
|
-
abortListener = () => controller.abort(signal.reason);
|
|
369
|
-
signal.addEventListener("abort", abortListener, { once: true });
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const cleanup = () => {
|
|
373
|
-
clearTimeout(timeout);
|
|
374
|
-
if (abortListener && signal) {
|
|
375
|
-
signal.removeEventListener("abort", abortListener);
|
|
376
|
-
}
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
return { controller, cleanup };
|
|
362
|
+
function createRequestSignal(signal: AbortSignal | undefined, timeoutSeconds: number): AbortSignal {
|
|
363
|
+
const timeoutSignal = AbortSignal.timeout(timeoutSeconds * 1000);
|
|
364
|
+
return signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
380
365
|
}
|
|
381
366
|
|
|
382
367
|
export const geminiImageTool: CustomTool<typeof geminiImageSchema, GeminiImageToolDetails> = {
|
|
@@ -404,118 +389,28 @@ export const geminiImageTool: CustomTool<typeof geminiImageSchema, GeminiImageTo
|
|
|
404
389
|
}
|
|
405
390
|
|
|
406
391
|
const timeoutSeconds = params.timeout_seconds ?? DEFAULT_TIMEOUT_SECONDS;
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
try {
|
|
410
|
-
if (provider === "openrouter") {
|
|
411
|
-
const contentParts: OpenRouterContentPart[] = [{ type: "text", text: params.prompt }];
|
|
412
|
-
for (const image of resolvedImages) {
|
|
413
|
-
contentParts.push({ type: "image_url", image_url: { url: toDataUrl(image) } });
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const requestBody = {
|
|
417
|
-
model: resolvedModel,
|
|
418
|
-
messages: [{ role: "user" as const, content: contentParts }],
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
422
|
-
method: "POST",
|
|
423
|
-
headers: {
|
|
424
|
-
"Content-Type": "application/json",
|
|
425
|
-
Authorization: `Bearer ${apiKey.apiKey}`,
|
|
426
|
-
},
|
|
427
|
-
body: JSON.stringify(requestBody),
|
|
428
|
-
signal: controller.signal,
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
const rawText = await response.text();
|
|
432
|
-
if (!response.ok) {
|
|
433
|
-
let message = rawText;
|
|
434
|
-
try {
|
|
435
|
-
const parsed = JSON.parse(rawText) as { error?: { message?: string } };
|
|
436
|
-
message = parsed.error?.message ?? message;
|
|
437
|
-
} catch {
|
|
438
|
-
// Keep raw text.
|
|
439
|
-
}
|
|
440
|
-
throw new Error(`OpenRouter image request failed (${response.status}): ${message}`);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const data = JSON.parse(rawText) as OpenRouterResponse;
|
|
444
|
-
const message = data.choices?.[0]?.message;
|
|
445
|
-
const responseText = collectOpenRouterResponseText(message);
|
|
446
|
-
const imageUrls = extractOpenRouterImageUrls(message);
|
|
447
|
-
const inlineImages: InlineImageData[] = [];
|
|
448
|
-
for (const imageUrl of imageUrls) {
|
|
449
|
-
inlineImages.push(await loadImageFromUrl(imageUrl, controller.signal));
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (inlineImages.length === 0) {
|
|
453
|
-
const messageText = responseText ? `\n\n${responseText}` : "";
|
|
454
|
-
return {
|
|
455
|
-
content: [{ type: "text", text: `No image data returned.${messageText}` }],
|
|
456
|
-
details: {
|
|
457
|
-
provider,
|
|
458
|
-
model: resolvedModel,
|
|
459
|
-
imageCount: 0,
|
|
460
|
-
imagePaths: [],
|
|
461
|
-
images: [],
|
|
462
|
-
responseText,
|
|
463
|
-
},
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const imagePaths = inlineImages.map(saveImageToTemp);
|
|
468
|
-
|
|
469
|
-
return {
|
|
470
|
-
content: [{ type: "text", text: buildResponseSummary(resolvedModel, imagePaths, responseText) }],
|
|
471
|
-
details: {
|
|
472
|
-
provider,
|
|
473
|
-
model: resolvedModel,
|
|
474
|
-
imageCount: inlineImages.length,
|
|
475
|
-
imagePaths,
|
|
476
|
-
images: inlineImages,
|
|
477
|
-
responseText,
|
|
478
|
-
},
|
|
479
|
-
};
|
|
480
|
-
}
|
|
392
|
+
const requestSignal = createRequestSignal(signal, timeoutSeconds);
|
|
481
393
|
|
|
482
|
-
|
|
394
|
+
if (provider === "openrouter") {
|
|
395
|
+
const contentParts: OpenRouterContentPart[] = [{ type: "text", text: params.prompt }];
|
|
483
396
|
for (const image of resolvedImages) {
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
parts.push({ text: params.prompt });
|
|
487
|
-
|
|
488
|
-
const generationConfig: {
|
|
489
|
-
responseModalities: GeminiResponseModality[];
|
|
490
|
-
imageConfig?: { aspectRatio?: string; imageSize?: string };
|
|
491
|
-
} = {
|
|
492
|
-
responseModalities: ["Image"],
|
|
493
|
-
};
|
|
494
|
-
|
|
495
|
-
if (params.aspect_ratio || params.image_size) {
|
|
496
|
-
generationConfig.imageConfig = {
|
|
497
|
-
aspectRatio: params.aspect_ratio,
|
|
498
|
-
imageSize: params.image_size,
|
|
499
|
-
};
|
|
397
|
+
contentParts.push({ type: "image_url", image_url: { url: toDataUrl(image) } });
|
|
500
398
|
}
|
|
501
399
|
|
|
502
400
|
const requestBody = {
|
|
503
|
-
|
|
504
|
-
|
|
401
|
+
model: resolvedModel,
|
|
402
|
+
messages: [{ role: "user" as const, content: contentParts }],
|
|
505
403
|
};
|
|
506
404
|
|
|
507
|
-
const response = await fetch(
|
|
508
|
-
|
|
509
|
-
{
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
"Content-Type": "application/json",
|
|
513
|
-
"x-goog-api-key": apiKey.apiKey,
|
|
514
|
-
},
|
|
515
|
-
body: JSON.stringify(requestBody),
|
|
516
|
-
signal: controller.signal,
|
|
405
|
+
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
406
|
+
method: "POST",
|
|
407
|
+
headers: {
|
|
408
|
+
"Content-Type": "application/json",
|
|
409
|
+
Authorization: `Bearer ${apiKey.apiKey}`,
|
|
517
410
|
},
|
|
518
|
-
|
|
411
|
+
body: JSON.stringify(requestBody),
|
|
412
|
+
signal: requestSignal,
|
|
413
|
+
});
|
|
519
414
|
|
|
520
415
|
const rawText = await response.text();
|
|
521
416
|
if (!response.ok) {
|
|
@@ -526,51 +421,137 @@ export const geminiImageTool: CustomTool<typeof geminiImageSchema, GeminiImageTo
|
|
|
526
421
|
} catch {
|
|
527
422
|
// Keep raw text.
|
|
528
423
|
}
|
|
529
|
-
throw new Error(`
|
|
424
|
+
throw new Error(`OpenRouter image request failed (${response.status}): ${message}`);
|
|
530
425
|
}
|
|
531
426
|
|
|
532
|
-
const data = JSON.parse(rawText) as
|
|
533
|
-
const
|
|
534
|
-
const responseText =
|
|
535
|
-
const
|
|
427
|
+
const data = JSON.parse(rawText) as OpenRouterResponse;
|
|
428
|
+
const message = data.choices?.[0]?.message;
|
|
429
|
+
const responseText = collectOpenRouterResponseText(message);
|
|
430
|
+
const imageUrls = extractOpenRouterImageUrls(message);
|
|
431
|
+
const inlineImages: InlineImageData[] = [];
|
|
432
|
+
for (const imageUrl of imageUrls) {
|
|
433
|
+
inlineImages.push(await loadImageFromUrl(imageUrl, requestSignal));
|
|
434
|
+
}
|
|
536
435
|
|
|
537
436
|
if (inlineImages.length === 0) {
|
|
538
|
-
const
|
|
539
|
-
? `Blocked: ${data.promptFeedback.blockReason}`
|
|
540
|
-
: "No image data returned.";
|
|
437
|
+
const messageText = responseText ? `\n\n${responseText}` : "";
|
|
541
438
|
return {
|
|
542
|
-
content: [{ type: "text", text:
|
|
439
|
+
content: [{ type: "text", text: `No image data returned.${messageText}` }],
|
|
543
440
|
details: {
|
|
544
441
|
provider,
|
|
545
|
-
model,
|
|
442
|
+
model: resolvedModel,
|
|
546
443
|
imageCount: 0,
|
|
547
444
|
imagePaths: [],
|
|
548
445
|
images: [],
|
|
549
446
|
responseText,
|
|
550
|
-
promptFeedback: data.promptFeedback,
|
|
551
|
-
usage: data.usageMetadata,
|
|
552
447
|
},
|
|
553
448
|
};
|
|
554
449
|
}
|
|
555
450
|
|
|
556
|
-
const imagePaths = inlineImages
|
|
451
|
+
const imagePaths = await saveImagesToTemp(inlineImages);
|
|
557
452
|
|
|
558
453
|
return {
|
|
559
|
-
content: [{ type: "text", text: buildResponseSummary(
|
|
454
|
+
content: [{ type: "text", text: buildResponseSummary(resolvedModel, imagePaths, responseText) }],
|
|
560
455
|
details: {
|
|
561
456
|
provider,
|
|
562
|
-
model,
|
|
457
|
+
model: resolvedModel,
|
|
563
458
|
imageCount: inlineImages.length,
|
|
564
459
|
imagePaths,
|
|
565
460
|
images: inlineImages,
|
|
566
461
|
responseText,
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const parts = [] as Array<{ text?: string; inlineData?: InlineImageData }>;
|
|
467
|
+
for (const image of resolvedImages) {
|
|
468
|
+
parts.push({ inlineData: image });
|
|
469
|
+
}
|
|
470
|
+
parts.push({ text: params.prompt });
|
|
471
|
+
|
|
472
|
+
const generationConfig: {
|
|
473
|
+
responseModalities: GeminiResponseModality[];
|
|
474
|
+
imageConfig?: { aspectRatio?: string; imageSize?: string };
|
|
475
|
+
} = {
|
|
476
|
+
responseModalities: ["Image"],
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
if (params.aspect_ratio || params.image_size) {
|
|
480
|
+
generationConfig.imageConfig = {
|
|
481
|
+
aspectRatio: params.aspect_ratio,
|
|
482
|
+
imageSize: params.image_size,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const requestBody = {
|
|
487
|
+
contents: [{ role: "user" as const, parts }],
|
|
488
|
+
generationConfig,
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const response = await fetch(
|
|
492
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`,
|
|
493
|
+
{
|
|
494
|
+
method: "POST",
|
|
495
|
+
headers: {
|
|
496
|
+
"Content-Type": "application/json",
|
|
497
|
+
"x-goog-api-key": apiKey.apiKey,
|
|
498
|
+
},
|
|
499
|
+
body: JSON.stringify(requestBody),
|
|
500
|
+
signal: requestSignal,
|
|
501
|
+
},
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
const rawText = await response.text();
|
|
505
|
+
if (!response.ok) {
|
|
506
|
+
let message = rawText;
|
|
507
|
+
try {
|
|
508
|
+
const parsed = JSON.parse(rawText) as { error?: { message?: string } };
|
|
509
|
+
message = parsed.error?.message ?? message;
|
|
510
|
+
} catch {
|
|
511
|
+
// Keep raw text.
|
|
512
|
+
}
|
|
513
|
+
throw new Error(`Gemini image request failed (${response.status}): ${message}`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const data = JSON.parse(rawText) as GeminiGenerateContentResponse;
|
|
517
|
+
const responseParts = combineParts(data);
|
|
518
|
+
const responseText = collectResponseText(responseParts);
|
|
519
|
+
const inlineImages = collectInlineImages(responseParts);
|
|
520
|
+
|
|
521
|
+
if (inlineImages.length === 0) {
|
|
522
|
+
const blocked = data.promptFeedback?.blockReason
|
|
523
|
+
? `Blocked: ${data.promptFeedback.blockReason}`
|
|
524
|
+
: "No image data returned.";
|
|
525
|
+
return {
|
|
526
|
+
content: [{ type: "text", text: `${blocked}${responseText ? `\n\n${responseText}` : ""}` }],
|
|
527
|
+
details: {
|
|
528
|
+
provider,
|
|
529
|
+
model,
|
|
530
|
+
imageCount: 0,
|
|
531
|
+
imagePaths: [],
|
|
532
|
+
images: [],
|
|
533
|
+
responseText,
|
|
567
534
|
promptFeedback: data.promptFeedback,
|
|
568
535
|
usage: data.usageMetadata,
|
|
569
536
|
},
|
|
570
537
|
};
|
|
571
|
-
} finally {
|
|
572
|
-
cleanup();
|
|
573
538
|
}
|
|
539
|
+
|
|
540
|
+
const imagePaths = await saveImagesToTemp(inlineImages);
|
|
541
|
+
|
|
542
|
+
return {
|
|
543
|
+
content: [{ type: "text", text: buildResponseSummary(model, imagePaths, responseText) }],
|
|
544
|
+
details: {
|
|
545
|
+
provider,
|
|
546
|
+
model,
|
|
547
|
+
imageCount: inlineImages.length,
|
|
548
|
+
imagePaths,
|
|
549
|
+
images: inlineImages,
|
|
550
|
+
responseText,
|
|
551
|
+
promptFeedback: data.promptFeedback,
|
|
552
|
+
usage: data.usageMetadata,
|
|
553
|
+
},
|
|
554
|
+
};
|
|
574
555
|
});
|
|
575
556
|
},
|
|
576
557
|
};
|