@oh-my-pi/pi-coding-agent 3.20.0 → 3.21.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 +78 -8
- package/docs/custom-tools.md +3 -3
- package/docs/extensions.md +226 -220
- package/docs/hooks.md +2 -2
- package/docs/sdk.md +3 -3
- package/examples/custom-tools/README.md +2 -2
- package/examples/custom-tools/subagent/index.ts +1 -1
- package/examples/extensions/README.md +76 -74
- package/examples/extensions/todo.ts +2 -5
- package/examples/hooks/custom-compaction.ts +1 -1
- package/examples/hooks/handoff.ts +1 -1
- package/examples/hooks/qna.ts +1 -1
- package/examples/sdk/02-custom-model.ts +1 -1
- package/examples/sdk/12-full-control.ts +1 -1
- package/examples/sdk/README.md +1 -1
- package/package.json +5 -5
- package/src/cli/file-processor.ts +1 -1
- package/src/cli/list-models.ts +1 -1
- package/src/core/agent-session.ts +13 -2
- package/src/core/auth-storage.ts +1 -1
- package/src/core/compaction/branch-summarization.ts +2 -2
- package/src/core/compaction/compaction.ts +2 -2
- package/src/core/compaction/utils.ts +1 -1
- package/src/core/custom-tools/types.ts +1 -1
- package/src/core/extensions/runner.ts +1 -1
- package/src/core/extensions/types.ts +1 -1
- package/src/core/extensions/wrapper.ts +1 -1
- package/src/core/hooks/runner.ts +2 -2
- package/src/core/hooks/types.ts +1 -1
- package/src/core/messages.ts +1 -1
- package/src/core/model-registry.ts +1 -1
- package/src/core/model-resolver.ts +1 -1
- package/src/core/sdk.ts +33 -4
- package/src/core/session-manager.ts +11 -22
- package/src/core/settings-manager.ts +66 -1
- package/src/core/slash-commands.ts +12 -5
- package/src/core/system-prompt.ts +27 -3
- package/src/core/title-generator.ts +2 -2
- package/src/core/tools/ask.ts +88 -1
- package/src/core/tools/bash-interceptor.ts +7 -0
- package/src/core/tools/bash.ts +106 -0
- package/src/core/tools/edit-diff.ts +73 -24
- package/src/core/tools/edit.ts +214 -20
- package/src/core/tools/find.ts +162 -1
- package/src/core/tools/gemini-image.ts +279 -56
- package/src/core/tools/git.ts +4 -0
- package/src/core/tools/grep.ts +191 -0
- package/src/core/tools/index.ts +3 -6
- package/src/core/tools/ls.ts +142 -2
- package/src/core/tools/lsp/render.ts +34 -14
- package/src/core/tools/notebook.ts +110 -0
- package/src/core/tools/output.ts +179 -7
- package/src/core/tools/read.ts +122 -9
- package/src/core/tools/render-utils.ts +241 -0
- package/src/core/tools/renderers.ts +40 -828
- package/src/core/tools/review.ts +26 -7
- package/src/core/tools/rulebook.ts +3 -1
- package/src/core/tools/task/index.ts +18 -3
- package/src/core/tools/task/render.ts +7 -2
- package/src/core/tools/task/types.ts +1 -1
- package/src/core/tools/truncate.ts +27 -1
- package/src/core/tools/web-fetch.ts +23 -15
- package/src/core/tools/web-search/index.ts +130 -45
- package/src/core/tools/web-search/providers/anthropic.ts +7 -2
- package/src/core/tools/web-search/providers/exa.ts +2 -1
- package/src/core/tools/web-search/providers/perplexity.ts +6 -1
- package/src/core/tools/web-search/render.ts +5 -0
- package/src/core/tools/web-search/types.ts +13 -0
- package/src/core/tools/write.ts +90 -0
- package/src/core/voice.ts +1 -1
- package/src/lib/worktree/constants.ts +6 -6
- package/src/main.ts +1 -1
- package/src/modes/interactive/components/assistant-message.ts +1 -1
- package/src/modes/interactive/components/custom-message.ts +1 -1
- package/src/modes/interactive/components/extensions/inspector-panel.ts +25 -22
- package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
- package/src/modes/interactive/components/footer.ts +1 -1
- package/src/modes/interactive/components/hook-message.ts +1 -1
- package/src/modes/interactive/components/model-selector.ts +1 -1
- package/src/modes/interactive/components/oauth-selector.ts +1 -1
- package/src/modes/interactive/components/settings-defs.ts +49 -0
- package/src/modes/interactive/components/status-line.ts +1 -1
- package/src/modes/interactive/components/tool-execution.ts +93 -538
- package/src/modes/interactive/interactive-mode.ts +19 -7
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +1 -1
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/prompts/system-prompt.md +4 -0
- package/src/prompts/tools/gemini-image.md +5 -1
- package/src/prompts/tools/output.md +4 -0
- package/src/prompts/tools/web-fetch.md +1 -0
- package/src/prompts/tools/web-search.md +2 -0
- package/src/utils/image-convert.ts +8 -2
- package/src/utils/image-magick.ts +247 -0
- package/src/utils/image-resize.ts +53 -13
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
2
5
|
import { type Static, Type } from "@sinclair/typebox";
|
|
3
6
|
import geminiImageDescription from "../../prompts/tools/gemini-image.md" with { type: "text" };
|
|
4
7
|
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime";
|
|
@@ -7,10 +10,17 @@ import { untilAborted } from "../utils";
|
|
|
7
10
|
import { resolveReadPath } from "./path-utils";
|
|
8
11
|
import { getEnv } from "./web-search/auth";
|
|
9
12
|
|
|
10
|
-
const DEFAULT_MODEL = "gemini-
|
|
13
|
+
const DEFAULT_MODEL = "gemini-3-pro-image-preview";
|
|
14
|
+
const DEFAULT_OPENROUTER_MODEL = "google/gemini-3-pro-image-preview";
|
|
11
15
|
const DEFAULT_TIMEOUT_SECONDS = 120;
|
|
12
16
|
const MAX_IMAGE_SIZE = 20 * 1024 * 1024;
|
|
13
17
|
|
|
18
|
+
type ImageProvider = "gemini" | "openrouter";
|
|
19
|
+
interface ImageApiKey {
|
|
20
|
+
provider: ImageProvider;
|
|
21
|
+
apiKey: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
const responseModalitySchema = Type.Union([Type.Literal("Image"), Type.Literal("Text")]);
|
|
15
25
|
const aspectRatioSchema = Type.Union(
|
|
16
26
|
[Type.Literal("1:1"), Type.Literal("3:4"), Type.Literal("4:3"), Type.Literal("9:16"), Type.Literal("16:9")],
|
|
@@ -34,13 +44,7 @@ export const geminiImageSchema = Type.Object(
|
|
|
34
44
|
prompt: Type.String({ description: "Text prompt for image generation or editing." }),
|
|
35
45
|
model: Type.Optional(
|
|
36
46
|
Type.String({
|
|
37
|
-
description: `
|
|
38
|
-
}),
|
|
39
|
-
),
|
|
40
|
-
response_modalities: Type.Optional(
|
|
41
|
-
Type.Array(responseModalitySchema, {
|
|
42
|
-
description: 'Response modalities (default: ["Image"]).',
|
|
43
|
-
minItems: 1,
|
|
47
|
+
description: `Image model. Default: ${DEFAULT_MODEL} (direct Gemini) or ${DEFAULT_OPENROUTER_MODEL} (OpenRouter).`,
|
|
44
48
|
}),
|
|
45
49
|
),
|
|
46
50
|
aspect_ratio: Type.Optional(aspectRatioSchema),
|
|
@@ -100,9 +104,35 @@ interface GeminiGenerateContentResponse {
|
|
|
100
104
|
usageMetadata?: GeminiUsageMetadata;
|
|
101
105
|
}
|
|
102
106
|
|
|
107
|
+
interface OpenRouterImageUrl {
|
|
108
|
+
url: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface OpenRouterContentPart {
|
|
112
|
+
type: "text" | "image_url";
|
|
113
|
+
text?: string;
|
|
114
|
+
image_url?: OpenRouterImageUrl;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface OpenRouterMessage {
|
|
118
|
+
content?: string | OpenRouterContentPart[];
|
|
119
|
+
images?: Array<string | { image_url?: OpenRouterImageUrl }>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface OpenRouterChoice {
|
|
123
|
+
message?: OpenRouterMessage;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface OpenRouterResponse {
|
|
127
|
+
choices?: OpenRouterChoice[];
|
|
128
|
+
}
|
|
129
|
+
|
|
103
130
|
interface GeminiImageToolDetails {
|
|
131
|
+
provider: ImageProvider;
|
|
104
132
|
model: string;
|
|
105
133
|
imageCount: number;
|
|
134
|
+
imagePaths: string[];
|
|
135
|
+
images: InlineImageData[];
|
|
106
136
|
responseText?: string;
|
|
107
137
|
promptFeedback?: GeminiPromptFeedback;
|
|
108
138
|
usage?: GeminiUsageMetadata;
|
|
@@ -125,12 +155,109 @@ function normalizeDataUrl(data: string): { data: string; mimeType?: string } {
|
|
|
125
155
|
return { data: match[2] ?? "", mimeType: match[1] };
|
|
126
156
|
}
|
|
127
157
|
|
|
128
|
-
|
|
158
|
+
function resolveOpenRouterModel(model: string): string {
|
|
159
|
+
return model.includes("/") ? model : `google/${model}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function toDataUrl(image: InlineImageData): string {
|
|
163
|
+
return `data:${image.mimeType};base64,${image.data}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function loadImageFromUrl(imageUrl: string, signal?: AbortSignal): Promise<InlineImageData> {
|
|
167
|
+
if (imageUrl.startsWith("data:")) {
|
|
168
|
+
const normalized = normalizeDataUrl(imageUrl.trim());
|
|
169
|
+
if (!normalized.mimeType) {
|
|
170
|
+
throw new Error("mime_type is required when providing raw base64 data.");
|
|
171
|
+
}
|
|
172
|
+
if (!normalized.data) {
|
|
173
|
+
throw new Error("Image data is empty.");
|
|
174
|
+
}
|
|
175
|
+
return { data: normalized.data, mimeType: normalized.mimeType };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const response = await fetch(imageUrl, { signal });
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
const rawText = await response.text();
|
|
181
|
+
throw new Error(`Image download failed (${response.status}): ${rawText}`);
|
|
182
|
+
}
|
|
183
|
+
const contentType = response.headers.get("content-type")?.split(";")[0];
|
|
184
|
+
if (!contentType || !contentType.startsWith("image/")) {
|
|
185
|
+
throw new Error(`Unsupported image type from URL: ${imageUrl}`);
|
|
186
|
+
}
|
|
187
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
188
|
+
return { data: buffer.toString("base64"), mimeType: contentType };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function collectOpenRouterResponseText(message: OpenRouterMessage | undefined): string | undefined {
|
|
192
|
+
if (!message) return undefined;
|
|
193
|
+
if (typeof message.content === "string") {
|
|
194
|
+
const trimmed = message.content.trim();
|
|
195
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
196
|
+
}
|
|
197
|
+
if (Array.isArray(message.content)) {
|
|
198
|
+
const texts = message.content
|
|
199
|
+
.filter((part) => part.type === "text")
|
|
200
|
+
.map((part) => part.text)
|
|
201
|
+
.filter((text): text is string => Boolean(text));
|
|
202
|
+
const combined = texts.join("\n").trim();
|
|
203
|
+
return combined.length > 0 ? combined : undefined;
|
|
204
|
+
}
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function extractOpenRouterImageUrls(message: OpenRouterMessage | undefined): string[] {
|
|
209
|
+
const urls: string[] = [];
|
|
210
|
+
if (!message) return urls;
|
|
211
|
+
for (const image of message.images ?? []) {
|
|
212
|
+
if (typeof image === "string") {
|
|
213
|
+
urls.push(image);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (image.image_url?.url) {
|
|
217
|
+
urls.push(image.image_url.url);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (Array.isArray(message.content)) {
|
|
221
|
+
for (const part of message.content) {
|
|
222
|
+
if (part.type === "image_url" && part.image_url?.url) {
|
|
223
|
+
urls.push(part.image_url.url);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return urls;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Preferred provider set via settings (default: auto) */
|
|
231
|
+
let preferredImageProvider: ImageProvider | "auto" = "auto";
|
|
232
|
+
|
|
233
|
+
/** Set the preferred image provider from settings */
|
|
234
|
+
export function setPreferredImageProvider(provider: ImageProvider | "auto"): void {
|
|
235
|
+
preferredImageProvider = provider;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function findImageApiKey(): Promise<ImageApiKey | null> {
|
|
239
|
+
// If a specific provider is preferred, try it first
|
|
240
|
+
if (preferredImageProvider === "gemini") {
|
|
241
|
+
const geminiKey = await getEnv("GEMINI_API_KEY");
|
|
242
|
+
if (geminiKey) return { provider: "gemini", apiKey: geminiKey };
|
|
243
|
+
const googleKey = await getEnv("GOOGLE_API_KEY");
|
|
244
|
+
if (googleKey) return { provider: "gemini", apiKey: googleKey };
|
|
245
|
+
// Fall through to auto-detect if preferred provider key not found
|
|
246
|
+
} else if (preferredImageProvider === "openrouter") {
|
|
247
|
+
const openRouterKey = await getEnv("OPENROUTER_API_KEY");
|
|
248
|
+
if (openRouterKey) return { provider: "openrouter", apiKey: openRouterKey };
|
|
249
|
+
// Fall through to auto-detect if preferred provider key not found
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Auto-detect: OpenRouter takes priority
|
|
253
|
+
const openRouterKey = await getEnv("OPENROUTER_API_KEY");
|
|
254
|
+
if (openRouterKey) return { provider: "openrouter", apiKey: openRouterKey };
|
|
255
|
+
|
|
129
256
|
const geminiKey = await getEnv("GEMINI_API_KEY");
|
|
130
|
-
if (geminiKey) return geminiKey;
|
|
257
|
+
if (geminiKey) return { provider: "gemini", apiKey: geminiKey };
|
|
131
258
|
|
|
132
259
|
const googleKey = await getEnv("GOOGLE_API_KEY");
|
|
133
|
-
if (googleKey) return googleKey;
|
|
260
|
+
if (googleKey) return { provider: "gemini", apiKey: googleKey };
|
|
134
261
|
|
|
135
262
|
return null;
|
|
136
263
|
}
|
|
@@ -174,8 +301,29 @@ async function resolveInputImage(input: ImageInput, cwd: string): Promise<Inline
|
|
|
174
301
|
throw new Error("input_images entries must include either path or data.");
|
|
175
302
|
}
|
|
176
303
|
|
|
177
|
-
function
|
|
178
|
-
const
|
|
304
|
+
function getExtensionForMime(mimeType: string): string {
|
|
305
|
+
const map: Record<string, string> = {
|
|
306
|
+
"image/png": "png",
|
|
307
|
+
"image/jpeg": "jpg",
|
|
308
|
+
"image/gif": "gif",
|
|
309
|
+
"image/webp": "webp",
|
|
310
|
+
};
|
|
311
|
+
return map[mimeType] ?? "png";
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function saveImageToTemp(image: InlineImageData): string {
|
|
315
|
+
const ext = getExtensionForMime(image.mimeType);
|
|
316
|
+
const filename = `omp-image-${crypto.randomUUID()}.${ext}`;
|
|
317
|
+
const filepath = join(tmpdir(), filename);
|
|
318
|
+
fs.writeFileSync(filepath, Buffer.from(image.data, "base64"));
|
|
319
|
+
return filepath;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function buildResponseSummary(model: string, imagePaths: string[], responseText: string | undefined): string {
|
|
323
|
+
const lines = [`Model: ${model}`, `Generated ${imagePaths.length} image(s):`];
|
|
324
|
+
for (const p of imagePaths) {
|
|
325
|
+
lines.push(` ${p}`);
|
|
326
|
+
}
|
|
179
327
|
if (responseText) {
|
|
180
328
|
lines.push("", responseText.trim());
|
|
181
329
|
}
|
|
@@ -232,60 +380,137 @@ function createAbortController(
|
|
|
232
380
|
}
|
|
233
381
|
|
|
234
382
|
export const geminiImageTool: CustomTool<typeof geminiImageSchema, GeminiImageToolDetails> = {
|
|
235
|
-
name: "
|
|
236
|
-
label: "
|
|
383
|
+
name: "generate_image",
|
|
384
|
+
label: "GenerateImage",
|
|
237
385
|
description: geminiImageDescription,
|
|
238
386
|
parameters: geminiImageSchema,
|
|
239
387
|
async execute(_toolCallId, params, _onUpdate, ctx, signal) {
|
|
240
388
|
return untilAborted(signal, async () => {
|
|
241
|
-
const apiKey = await
|
|
389
|
+
const apiKey = await findImageApiKey();
|
|
242
390
|
if (!apiKey) {
|
|
243
|
-
throw new Error("GEMINI_API_KEY not found.");
|
|
391
|
+
throw new Error("OPENROUTER_API_KEY, GEMINI_API_KEY, or GOOGLE_API_KEY not found.");
|
|
244
392
|
}
|
|
245
393
|
|
|
246
|
-
const
|
|
247
|
-
const
|
|
394
|
+
const provider = apiKey.provider;
|
|
395
|
+
const model = params.model ?? (provider === "openrouter" ? DEFAULT_OPENROUTER_MODEL : DEFAULT_MODEL);
|
|
396
|
+
const resolvedModel = provider === "openrouter" ? resolveOpenRouterModel(model) : model;
|
|
248
397
|
const cwd = ctx.sessionManager.getCwd();
|
|
249
398
|
|
|
250
|
-
const
|
|
399
|
+
const resolvedImages: InlineImageData[] = [];
|
|
251
400
|
if (params.input_images?.length) {
|
|
252
401
|
for (const input of params.input_images) {
|
|
253
|
-
|
|
254
|
-
parts.push({ inlineData: image });
|
|
402
|
+
resolvedImages.push(await resolveInputImage(input, cwd));
|
|
255
403
|
}
|
|
256
404
|
}
|
|
257
|
-
parts.push({ text: params.prompt });
|
|
258
|
-
|
|
259
|
-
const generationConfig: {
|
|
260
|
-
responseModalities: GeminiResponseModality[];
|
|
261
|
-
imageConfig?: { aspectRatio?: string; imageSize?: string };
|
|
262
|
-
} = {
|
|
263
|
-
responseModalities,
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
if (params.aspect_ratio || params.image_size) {
|
|
267
|
-
generationConfig.imageConfig = {
|
|
268
|
-
aspectRatio: params.aspect_ratio,
|
|
269
|
-
imageSize: params.image_size,
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const requestBody = {
|
|
274
|
-
contents: [{ role: "user" as const, parts }],
|
|
275
|
-
generationConfig,
|
|
276
|
-
};
|
|
277
405
|
|
|
278
406
|
const timeoutSeconds = params.timeout_seconds ?? DEFAULT_TIMEOUT_SECONDS;
|
|
279
407
|
const { controller, cleanup } = createAbortController(signal, timeoutSeconds);
|
|
280
408
|
|
|
281
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
|
+
}
|
|
481
|
+
|
|
482
|
+
const parts = [] as Array<{ text?: string; inlineData?: InlineImageData }>;
|
|
483
|
+
for (const image of resolvedImages) {
|
|
484
|
+
parts.push({ inlineData: image });
|
|
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
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const requestBody = {
|
|
503
|
+
contents: [{ role: "user" as const, parts }],
|
|
504
|
+
generationConfig,
|
|
505
|
+
};
|
|
506
|
+
|
|
282
507
|
const response = await fetch(
|
|
283
508
|
`https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`,
|
|
284
509
|
{
|
|
285
510
|
method: "POST",
|
|
286
511
|
headers: {
|
|
287
512
|
"Content-Type": "application/json",
|
|
288
|
-
"x-goog-api-key": apiKey,
|
|
513
|
+
"x-goog-api-key": apiKey.apiKey,
|
|
289
514
|
},
|
|
290
515
|
body: JSON.stringify(requestBody),
|
|
291
516
|
signal: controller.signal,
|
|
@@ -308,18 +533,19 @@ export const geminiImageTool: CustomTool<typeof geminiImageSchema, GeminiImageTo
|
|
|
308
533
|
const responseParts = combineParts(data);
|
|
309
534
|
const responseText = collectResponseText(responseParts);
|
|
310
535
|
const inlineImages = collectInlineImages(responseParts);
|
|
311
|
-
const content: Array<TextContent | ImageContent> = [];
|
|
312
536
|
|
|
313
537
|
if (inlineImages.length === 0) {
|
|
314
538
|
const blocked = data.promptFeedback?.blockReason
|
|
315
539
|
? `Blocked: ${data.promptFeedback.blockReason}`
|
|
316
540
|
: "No image data returned.";
|
|
317
|
-
content.push({ type: "text", text: `${blocked}${responseText ? `\n\n${responseText}` : ""}` });
|
|
318
541
|
return {
|
|
319
|
-
content,
|
|
542
|
+
content: [{ type: "text", text: `${blocked}${responseText ? `\n\n${responseText}` : ""}` }],
|
|
320
543
|
details: {
|
|
544
|
+
provider,
|
|
321
545
|
model,
|
|
322
546
|
imageCount: 0,
|
|
547
|
+
imagePaths: [],
|
|
548
|
+
images: [],
|
|
323
549
|
responseText,
|
|
324
550
|
promptFeedback: data.promptFeedback,
|
|
325
551
|
usage: data.usageMetadata,
|
|
@@ -327,19 +553,16 @@ export const geminiImageTool: CustomTool<typeof geminiImageSchema, GeminiImageTo
|
|
|
327
553
|
};
|
|
328
554
|
}
|
|
329
555
|
|
|
330
|
-
|
|
331
|
-
type: "text",
|
|
332
|
-
text: buildResponseSummary(model, inlineImages.length, responseText),
|
|
333
|
-
});
|
|
334
|
-
for (const image of inlineImages) {
|
|
335
|
-
content.push({ type: "image", data: image.data, mimeType: image.mimeType });
|
|
336
|
-
}
|
|
556
|
+
const imagePaths = inlineImages.map(saveImageToTemp);
|
|
337
557
|
|
|
338
558
|
return {
|
|
339
|
-
content,
|
|
559
|
+
content: [{ type: "text", text: buildResponseSummary(model, imagePaths, responseText) }],
|
|
340
560
|
details: {
|
|
561
|
+
provider,
|
|
341
562
|
model,
|
|
342
563
|
imageCount: inlineImages.length,
|
|
564
|
+
imagePaths,
|
|
565
|
+
images: inlineImages,
|
|
343
566
|
responseText,
|
|
344
567
|
promptFeedback: data.promptFeedback,
|
|
345
568
|
usage: data.usageMetadata,
|
|
@@ -355,7 +578,7 @@ export const geminiImageTool: CustomTool<typeof geminiImageSchema, GeminiImageTo
|
|
|
355
578
|
export async function getGeminiImageTools(): Promise<
|
|
356
579
|
Array<CustomTool<typeof geminiImageSchema, GeminiImageToolDetails>>
|
|
357
580
|
> {
|
|
358
|
-
const apiKey = await
|
|
581
|
+
const apiKey = await findImageApiKey();
|
|
359
582
|
if (!apiKey) return [];
|
|
360
583
|
return [geminiImageTool];
|
|
361
584
|
}
|
package/src/core/tools/git.ts
CHANGED
|
@@ -199,6 +199,10 @@ export function createGitTool(cwd: string): AgentTool<typeof gitSchema, GitToolD
|
|
|
199
199
|
description: gitDescription,
|
|
200
200
|
parameters: gitSchema,
|
|
201
201
|
execute: async (_toolCallId, params: Static<typeof gitSchema>, _signal?: AbortSignal) => {
|
|
202
|
+
if (params.operation === "commit" && !params.message) {
|
|
203
|
+
throw new Error("Git commit requires a message to avoid an interactive editor. Provide `message`.");
|
|
204
|
+
}
|
|
205
|
+
|
|
202
206
|
const result = await gitToolCore(params as GitParams, cwd);
|
|
203
207
|
if ("error" in result) {
|
|
204
208
|
const message = result._rendered ?? result.error;
|
package/src/core/tools/grep.ts
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import { readFileSync, type Stats, statSync } from "node:fs";
|
|
2
2
|
import nodePath from "node:path";
|
|
3
3
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
4
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
5
|
+
import { Text } from "@oh-my-pi/pi-tui";
|
|
4
6
|
import { Type } from "@sinclair/typebox";
|
|
5
7
|
import type { Subprocess } from "bun";
|
|
8
|
+
import { getLanguageFromPath, type Theme } from "../../modes/interactive/theme/theme";
|
|
6
9
|
import grepDescription from "../../prompts/tools/grep.md" with { type: "text" };
|
|
7
10
|
import { ensureTool } from "../../utils/tools-manager";
|
|
11
|
+
import type { RenderResultOptions } from "../custom-tools/types";
|
|
8
12
|
import { resolveToCwd } from "./path-utils";
|
|
13
|
+
import {
|
|
14
|
+
formatCount,
|
|
15
|
+
formatEmptyMessage,
|
|
16
|
+
formatErrorMessage,
|
|
17
|
+
formatExpandHint,
|
|
18
|
+
formatMeta,
|
|
19
|
+
formatMoreItems,
|
|
20
|
+
formatScope,
|
|
21
|
+
formatTruncationSuffix,
|
|
22
|
+
PREVIEW_LIMITS,
|
|
23
|
+
} from "./render-utils";
|
|
9
24
|
import {
|
|
10
25
|
DEFAULT_MAX_BYTES,
|
|
11
26
|
formatSize,
|
|
@@ -582,3 +597,179 @@ export function createGrepTool(cwd: string): AgentTool<typeof grepSchema> {
|
|
|
582
597
|
|
|
583
598
|
/** Default grep tool using process.cwd() - for backwards compatibility */
|
|
584
599
|
export const grepTool = createGrepTool(process.cwd());
|
|
600
|
+
|
|
601
|
+
// =============================================================================
|
|
602
|
+
// TUI Renderer
|
|
603
|
+
// =============================================================================
|
|
604
|
+
|
|
605
|
+
interface GrepRenderArgs {
|
|
606
|
+
pattern: string;
|
|
607
|
+
path?: string;
|
|
608
|
+
glob?: string;
|
|
609
|
+
type?: string;
|
|
610
|
+
ignoreCase?: boolean;
|
|
611
|
+
caseSensitive?: boolean;
|
|
612
|
+
literal?: boolean;
|
|
613
|
+
multiline?: boolean;
|
|
614
|
+
context?: number;
|
|
615
|
+
limit?: number;
|
|
616
|
+
outputMode?: string;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
620
|
+
const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
|
621
|
+
|
|
622
|
+
export const grepToolRenderer = {
|
|
623
|
+
renderCall(args: GrepRenderArgs, uiTheme: Theme): Component {
|
|
624
|
+
const label = uiTheme.fg("toolTitle", uiTheme.bold("Grep"));
|
|
625
|
+
let text = `${label} ${uiTheme.fg("accent", args.pattern || "?")}`;
|
|
626
|
+
|
|
627
|
+
const meta: string[] = [];
|
|
628
|
+
if (args.path) meta.push(`in ${args.path}`);
|
|
629
|
+
if (args.glob) meta.push(`glob:${args.glob}`);
|
|
630
|
+
if (args.type) meta.push(`type:${args.type}`);
|
|
631
|
+
if (args.outputMode && args.outputMode !== "files_with_matches") meta.push(`mode:${args.outputMode}`);
|
|
632
|
+
if (args.caseSensitive) {
|
|
633
|
+
meta.push("case:sensitive");
|
|
634
|
+
} else if (args.ignoreCase) {
|
|
635
|
+
meta.push("case:insensitive");
|
|
636
|
+
}
|
|
637
|
+
if (args.literal) meta.push("literal");
|
|
638
|
+
if (args.multiline) meta.push("multiline");
|
|
639
|
+
if (args.context !== undefined) meta.push(`context:${args.context}`);
|
|
640
|
+
if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
|
|
641
|
+
|
|
642
|
+
text += formatMeta(meta, uiTheme);
|
|
643
|
+
|
|
644
|
+
return new Text(text, 0, 0);
|
|
645
|
+
},
|
|
646
|
+
|
|
647
|
+
renderResult(
|
|
648
|
+
result: { content: Array<{ type: string; text?: string }>; details?: GrepToolDetails },
|
|
649
|
+
{ expanded }: RenderResultOptions,
|
|
650
|
+
uiTheme: Theme,
|
|
651
|
+
): Component {
|
|
652
|
+
const details = result.details;
|
|
653
|
+
|
|
654
|
+
if (details?.error) {
|
|
655
|
+
return new Text(formatErrorMessage(details.error, uiTheme), 0, 0);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const hasDetailedData = details?.matchCount !== undefined || details?.fileCount !== undefined;
|
|
659
|
+
|
|
660
|
+
if (!hasDetailedData) {
|
|
661
|
+
const textContent = result.content?.find((c) => c.type === "text")?.text;
|
|
662
|
+
if (!textContent || textContent === "No matches found") {
|
|
663
|
+
return new Text(formatEmptyMessage("No matches found", uiTheme), 0, 0);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const lines = textContent.split("\n").filter((line) => line.trim() !== "");
|
|
667
|
+
const maxLines = expanded ? lines.length : Math.min(lines.length, COLLAPSED_TEXT_LIMIT);
|
|
668
|
+
const displayLines = lines.slice(0, maxLines);
|
|
669
|
+
const remaining = lines.length - maxLines;
|
|
670
|
+
const hasMore = remaining > 0;
|
|
671
|
+
|
|
672
|
+
const icon = uiTheme.styledSymbol("status.success", "success");
|
|
673
|
+
const summary = formatCount("item", lines.length);
|
|
674
|
+
const expandHint = formatExpandHint(expanded, hasMore, uiTheme);
|
|
675
|
+
let text = `${icon} ${uiTheme.fg("dim", summary)}${expandHint}`;
|
|
676
|
+
|
|
677
|
+
for (let i = 0; i < displayLines.length; i++) {
|
|
678
|
+
const isLast = i === displayLines.length - 1 && remaining === 0;
|
|
679
|
+
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
680
|
+
text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("toolOutput", displayLines[i])}`;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (remaining > 0) {
|
|
684
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
|
|
685
|
+
"muted",
|
|
686
|
+
formatMoreItems(remaining, "item", uiTheme),
|
|
687
|
+
)}`;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return new Text(text, 0, 0);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const matchCount = details?.matchCount ?? 0;
|
|
694
|
+
const fileCount = details?.fileCount ?? 0;
|
|
695
|
+
const mode = details?.mode ?? "files_with_matches";
|
|
696
|
+
const truncated = details?.truncated ?? details?.truncation?.truncated ?? false;
|
|
697
|
+
const files = details?.files ?? [];
|
|
698
|
+
|
|
699
|
+
if (matchCount === 0) {
|
|
700
|
+
return new Text(formatEmptyMessage("No matches found", uiTheme), 0, 0);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const icon = uiTheme.styledSymbol("status.success", "success");
|
|
704
|
+
const summaryParts =
|
|
705
|
+
mode === "files_with_matches"
|
|
706
|
+
? [formatCount("file", fileCount)]
|
|
707
|
+
: [formatCount("match", matchCount), formatCount("file", fileCount)];
|
|
708
|
+
const summaryText = summaryParts.join(uiTheme.sep.dot);
|
|
709
|
+
const scopeLabel = formatScope(details?.scopePath, uiTheme);
|
|
710
|
+
|
|
711
|
+
const fileEntries: Array<{ path: string; count?: number }> = details?.fileMatches?.length
|
|
712
|
+
? details.fileMatches.map((entry) => ({ path: entry.path, count: entry.count }))
|
|
713
|
+
: files.map((path) => ({ path }));
|
|
714
|
+
const maxFiles = expanded ? fileEntries.length : Math.min(fileEntries.length, COLLAPSED_LIST_LIMIT);
|
|
715
|
+
const hasMoreFiles = fileEntries.length > maxFiles;
|
|
716
|
+
const expandHint = formatExpandHint(expanded, hasMoreFiles, uiTheme);
|
|
717
|
+
|
|
718
|
+
let text = `${icon} ${uiTheme.fg("dim", summaryText)}${formatTruncationSuffix(
|
|
719
|
+
truncated,
|
|
720
|
+
uiTheme,
|
|
721
|
+
)}${scopeLabel}${expandHint}`;
|
|
722
|
+
|
|
723
|
+
const truncationReasons: string[] = [];
|
|
724
|
+
if (details?.matchLimitReached) {
|
|
725
|
+
truncationReasons.push(`limit ${details.matchLimitReached} matches`);
|
|
726
|
+
}
|
|
727
|
+
if (details?.headLimitReached) {
|
|
728
|
+
truncationReasons.push(`head limit ${details.headLimitReached}`);
|
|
729
|
+
}
|
|
730
|
+
if (details?.truncation?.truncated) {
|
|
731
|
+
truncationReasons.push("size limit");
|
|
732
|
+
}
|
|
733
|
+
if (details?.linesTruncated) {
|
|
734
|
+
truncationReasons.push("line length");
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const hasTruncation = truncationReasons.length > 0;
|
|
738
|
+
|
|
739
|
+
if (fileEntries.length > 0) {
|
|
740
|
+
for (let i = 0; i < maxFiles; i++) {
|
|
741
|
+
const entry = fileEntries[i];
|
|
742
|
+
const isLast = i === maxFiles - 1 && !hasMoreFiles && !hasTruncation;
|
|
743
|
+
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
744
|
+
const isDir = entry.path.endsWith("/");
|
|
745
|
+
const entryPath = isDir ? entry.path.slice(0, -1) : entry.path;
|
|
746
|
+
const lang = isDir ? undefined : getLanguageFromPath(entryPath);
|
|
747
|
+
const entryIcon = isDir
|
|
748
|
+
? uiTheme.fg("accent", uiTheme.icon.folder)
|
|
749
|
+
: uiTheme.fg("muted", uiTheme.getLangIcon(lang));
|
|
750
|
+
const countLabel =
|
|
751
|
+
entry.count !== undefined
|
|
752
|
+
? ` ${uiTheme.fg("dim", `(${entry.count} match${entry.count !== 1 ? "es" : ""})`)}`
|
|
753
|
+
: "";
|
|
754
|
+
text += `\n ${uiTheme.fg("dim", branch)} ${entryIcon} ${uiTheme.fg("accent", entry.path)}${countLabel}`;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (hasMoreFiles) {
|
|
758
|
+
const moreFilesBranch = hasTruncation ? uiTheme.tree.branch : uiTheme.tree.last;
|
|
759
|
+
text += `\n ${uiTheme.fg("dim", moreFilesBranch)} ${uiTheme.fg(
|
|
760
|
+
"muted",
|
|
761
|
+
formatMoreItems(fileEntries.length - maxFiles, "file", uiTheme),
|
|
762
|
+
)}`;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (hasTruncation) {
|
|
767
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
|
|
768
|
+
"warning",
|
|
769
|
+
`truncated: ${truncationReasons.join(", ")}`,
|
|
770
|
+
)}`;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return new Text(text, 0, 0);
|
|
774
|
+
},
|
|
775
|
+
};
|
package/src/core/tools/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { createEditTool, type EditToolOptions, editTool } from "./edit";
|
|
|
5
5
|
export { exaTools } from "./exa/index";
|
|
6
6
|
export type { ExaRenderDetails, ExaSearchResponse, ExaSearchResult } from "./exa/types";
|
|
7
7
|
export { createFindTool, type FindToolDetails, findTool } from "./find";
|
|
8
|
+
export { setPreferredImageProvider } from "./gemini-image";
|
|
8
9
|
export { createGitTool, type GitToolDetails, gitTool } from "./git";
|
|
9
10
|
export { createGrepTool, type GrepToolDetails, grepTool } from "./grep";
|
|
10
11
|
export { createLsTool, type LsToolDetails, lsTool } from "./ls";
|
|
@@ -23,12 +24,7 @@ export { createNotebookTool, type NotebookToolDetails, notebookTool } from "./no
|
|
|
23
24
|
export { createOutputTool, type OutputToolDetails, outputTool } from "./output";
|
|
24
25
|
export { createReadTool, type ReadToolDetails, type ReadToolOptions, readTool } from "./read";
|
|
25
26
|
export { createReportFindingTool, createSubmitReviewTool, reportFindingTool, submitReviewTool } from "./review";
|
|
26
|
-
export {
|
|
27
|
-
createRulebookTool,
|
|
28
|
-
filterRulebookRules,
|
|
29
|
-
formatRulesForPrompt,
|
|
30
|
-
type RulebookToolDetails,
|
|
31
|
-
} from "./rulebook";
|
|
27
|
+
export { createRulebookTool, filterRulebookRules, formatRulesForPrompt, type RulebookToolDetails } from "./rulebook";
|
|
32
28
|
export { BUNDLED_AGENTS, createTaskTool, taskTool } from "./task/index";
|
|
33
29
|
export type { TruncationResult } from "./truncate";
|
|
34
30
|
export { createWebFetchTool, type WebFetchToolDetails, webFetchCustomTool, webFetchTool } from "./web-fetch";
|
|
@@ -39,6 +35,7 @@ export {
|
|
|
39
35
|
getWebSearchTools,
|
|
40
36
|
hasExaWebSearch,
|
|
41
37
|
linkedinWebSearchTools,
|
|
38
|
+
setPreferredWebSearchProvider,
|
|
42
39
|
type WebSearchProvider,
|
|
43
40
|
type WebSearchResponse,
|
|
44
41
|
type WebSearchToolsOptions,
|