@oh-my-pi/pi-coding-agent 3.15.1 → 3.20.1
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 +60 -0
- package/docs/extensions.md +1055 -0
- package/docs/rpc.md +69 -13
- package/docs/session-tree-plan.md +1 -1
- package/examples/extensions/README.md +141 -0
- package/examples/extensions/api-demo.ts +87 -0
- package/examples/extensions/chalk-logger.ts +26 -0
- package/examples/extensions/hello.ts +33 -0
- package/examples/extensions/pirate.ts +44 -0
- package/examples/extensions/plan-mode.ts +551 -0
- package/examples/extensions/subagent/agents/reviewer.md +35 -0
- package/examples/extensions/todo.ts +299 -0
- package/examples/extensions/tools.ts +145 -0
- package/examples/extensions/with-deps/index.ts +36 -0
- package/examples/extensions/with-deps/package-lock.json +31 -0
- package/examples/extensions/with-deps/package.json +16 -0
- package/examples/sdk/02-custom-model.ts +3 -3
- package/examples/sdk/05-tools.ts +7 -3
- package/examples/sdk/06-extensions.ts +81 -0
- package/examples/sdk/06-hooks.ts +14 -13
- package/examples/sdk/08-prompt-templates.ts +42 -0
- package/examples/sdk/08-slash-commands.ts +17 -12
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/examples/sdk/12-full-control.ts +6 -6
- package/package.json +11 -7
- package/src/capability/extension-module.ts +34 -0
- package/src/cli/args.ts +22 -7
- package/src/cli/file-processor.ts +38 -67
- package/src/cli/list-models.ts +1 -1
- package/src/config.ts +25 -14
- package/src/core/agent-session.ts +505 -242
- package/src/core/auth-storage.ts +33 -21
- package/src/core/compaction/branch-summarization.ts +4 -4
- package/src/core/compaction/compaction.ts +3 -3
- package/src/core/custom-commands/bundled/wt/index.ts +430 -0
- package/src/core/custom-commands/loader.ts +9 -0
- package/src/core/custom-tools/wrapper.ts +5 -0
- package/src/core/event-bus.ts +59 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/extensions/index.ts +100 -0
- package/src/core/extensions/loader.ts +501 -0
- package/src/core/extensions/runner.ts +477 -0
- package/src/core/extensions/types.ts +712 -0
- package/src/core/extensions/wrapper.ts +147 -0
- package/src/core/hooks/types.ts +2 -2
- package/src/core/index.ts +10 -21
- package/src/core/keybindings.ts +199 -0
- package/src/core/messages.ts +26 -7
- package/src/core/model-registry.ts +123 -46
- package/src/core/model-resolver.ts +7 -5
- package/src/core/prompt-templates.ts +242 -0
- package/src/core/sdk.ts +378 -295
- package/src/core/session-manager.ts +72 -58
- package/src/core/settings-manager.ts +118 -22
- package/src/core/system-prompt.ts +24 -1
- package/src/core/terminal-notify.ts +37 -0
- package/src/core/tools/context.ts +4 -4
- package/src/core/tools/exa/mcp-client.ts +5 -4
- package/src/core/tools/exa/render.ts +176 -131
- package/src/core/tools/find.ts +7 -1
- package/src/core/tools/gemini-image.ts +361 -0
- package/src/core/tools/git.ts +216 -0
- package/src/core/tools/index.ts +28 -15
- package/src/core/tools/ls.ts +9 -2
- package/src/core/tools/lsp/config.ts +5 -4
- package/src/core/tools/lsp/index.ts +17 -12
- package/src/core/tools/lsp/render.ts +39 -47
- package/src/core/tools/read.ts +66 -29
- package/src/core/tools/render-utils.ts +268 -0
- package/src/core/tools/renderers.ts +243 -225
- package/src/core/tools/task/discovery.ts +2 -2
- package/src/core/tools/task/executor.ts +66 -58
- package/src/core/tools/task/index.ts +29 -10
- package/src/core/tools/task/model-resolver.ts +8 -13
- package/src/core/tools/task/omp-command.ts +24 -0
- package/src/core/tools/task/render.ts +37 -62
- package/src/core/tools/task/types.ts +3 -0
- package/src/core/tools/web-fetch.ts +29 -28
- package/src/core/tools/web-search/index.ts +6 -5
- package/src/core/tools/web-search/providers/exa.ts +6 -5
- package/src/core/tools/web-search/render.ts +66 -111
- package/src/core/voice-controller.ts +135 -0
- package/src/core/voice-supervisor.ts +1003 -0
- package/src/core/voice.ts +308 -0
- package/src/discovery/builtin.ts +75 -1
- package/src/discovery/claude.ts +47 -1
- package/src/discovery/codex.ts +54 -2
- package/src/discovery/gemini.ts +55 -2
- package/src/discovery/helpers.ts +100 -1
- package/src/discovery/index.ts +2 -0
- package/src/index.ts +14 -9
- package/src/lib/worktree/collapse.ts +179 -0
- package/src/lib/worktree/constants.ts +14 -0
- package/src/lib/worktree/errors.ts +23 -0
- package/src/lib/worktree/git.ts +110 -0
- package/src/lib/worktree/index.ts +23 -0
- package/src/lib/worktree/operations.ts +216 -0
- package/src/lib/worktree/session.ts +114 -0
- package/src/lib/worktree/stats.ts +67 -0
- package/src/main.ts +61 -37
- package/src/migrations.ts +37 -7
- package/src/modes/interactive/components/bash-execution.ts +6 -4
- package/src/modes/interactive/components/custom-editor.ts +55 -0
- package/src/modes/interactive/components/custom-message.ts +95 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +5 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +18 -12
- package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
- package/src/modes/interactive/components/extensions/types.ts +1 -0
- package/src/modes/interactive/components/footer.ts +324 -0
- package/src/modes/interactive/components/hook-selector.ts +3 -3
- package/src/modes/interactive/components/model-selector.ts +7 -6
- package/src/modes/interactive/components/oauth-selector.ts +3 -3
- package/src/modes/interactive/components/settings-defs.ts +55 -6
- package/src/modes/interactive/components/status-line.ts +45 -37
- package/src/modes/interactive/components/tool-execution.ts +95 -23
- package/src/modes/interactive/interactive-mode.ts +643 -113
- package/src/modes/interactive/theme/defaults/index.ts +16 -16
- package/src/modes/print-mode.ts +14 -72
- package/src/modes/rpc/rpc-client.ts +23 -9
- package/src/modes/rpc/rpc-mode.ts +137 -125
- package/src/modes/rpc/rpc-types.ts +46 -24
- package/src/prompts/task.md +1 -0
- package/src/prompts/tools/gemini-image.md +4 -0
- package/src/prompts/tools/git.md +9 -0
- package/src/prompts/voice-summary.md +12 -0
- package/src/utils/image-convert.ts +26 -0
- package/src/utils/image-resize.ts +215 -0
- package/src/utils/shell-snapshot.ts +22 -20
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
3
|
+
import geminiImageDescription from "../../prompts/tools/gemini-image.md" with { type: "text" };
|
|
4
|
+
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime";
|
|
5
|
+
import type { CustomTool } from "../custom-tools/types";
|
|
6
|
+
import { untilAborted } from "../utils";
|
|
7
|
+
import { resolveReadPath } from "./path-utils";
|
|
8
|
+
import { getEnv } from "./web-search/auth";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_MODEL = "gemini-2.5-flash-image";
|
|
11
|
+
const DEFAULT_TIMEOUT_SECONDS = 120;
|
|
12
|
+
const MAX_IMAGE_SIZE = 20 * 1024 * 1024;
|
|
13
|
+
|
|
14
|
+
const responseModalitySchema = Type.Union([Type.Literal("Image"), Type.Literal("Text")]);
|
|
15
|
+
const aspectRatioSchema = Type.Union(
|
|
16
|
+
[Type.Literal("1:1"), Type.Literal("3:4"), Type.Literal("4:3"), Type.Literal("9:16"), Type.Literal("16:9")],
|
|
17
|
+
{ description: "Aspect ratio (1:1, 3:4, 4:3, 9:16, 16:9)." },
|
|
18
|
+
);
|
|
19
|
+
const imageSizeSchema = Type.Union([Type.Literal("1024x1024"), Type.Literal("1536x1024"), Type.Literal("1024x1536")], {
|
|
20
|
+
description: "Image size, mainly for gemini-3-pro-image-preview.",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const inputImageSchema = Type.Object(
|
|
24
|
+
{
|
|
25
|
+
path: Type.Optional(Type.String({ description: "Path to an input image file." })),
|
|
26
|
+
data: Type.Optional(Type.String({ description: "Base64 image data or a data: URL." })),
|
|
27
|
+
mime_type: Type.Optional(Type.String({ description: "Required for raw base64 data." })),
|
|
28
|
+
},
|
|
29
|
+
{ additionalProperties: false },
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export const geminiImageSchema = Type.Object(
|
|
33
|
+
{
|
|
34
|
+
prompt: Type.String({ description: "Text prompt for image generation or editing." }),
|
|
35
|
+
model: Type.Optional(
|
|
36
|
+
Type.String({
|
|
37
|
+
description: `Gemini image model. Default: ${DEFAULT_MODEL} (Nano Banana).`,
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
40
|
+
response_modalities: Type.Optional(
|
|
41
|
+
Type.Array(responseModalitySchema, {
|
|
42
|
+
description: 'Response modalities (default: ["Image"]).',
|
|
43
|
+
minItems: 1,
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
46
|
+
aspect_ratio: Type.Optional(aspectRatioSchema),
|
|
47
|
+
image_size: Type.Optional(imageSizeSchema),
|
|
48
|
+
input_images: Type.Optional(
|
|
49
|
+
Type.Array(inputImageSchema, {
|
|
50
|
+
description: "Optional input images for edits or variations.",
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
timeout_seconds: Type.Optional(
|
|
54
|
+
Type.Number({
|
|
55
|
+
description: `Request timeout in seconds (default: ${DEFAULT_TIMEOUT_SECONDS}).`,
|
|
56
|
+
minimum: 1,
|
|
57
|
+
maximum: 600,
|
|
58
|
+
}),
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
{ additionalProperties: false },
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
export type GeminiImageParams = Static<typeof geminiImageSchema>;
|
|
65
|
+
export type GeminiResponseModality = Static<typeof responseModalitySchema>;
|
|
66
|
+
|
|
67
|
+
interface GeminiInlineData {
|
|
68
|
+
data?: string;
|
|
69
|
+
mimeType?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface GeminiPart {
|
|
73
|
+
text?: string;
|
|
74
|
+
inlineData?: GeminiInlineData;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface GeminiCandidate {
|
|
78
|
+
content?: { parts?: GeminiPart[] };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface GeminiSafetyRating {
|
|
82
|
+
category?: string;
|
|
83
|
+
probability?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface GeminiPromptFeedback {
|
|
87
|
+
blockReason?: string;
|
|
88
|
+
safetyRatings?: GeminiSafetyRating[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface GeminiUsageMetadata {
|
|
92
|
+
promptTokenCount?: number;
|
|
93
|
+
candidatesTokenCount?: number;
|
|
94
|
+
totalTokenCount?: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface GeminiGenerateContentResponse {
|
|
98
|
+
candidates?: GeminiCandidate[];
|
|
99
|
+
promptFeedback?: GeminiPromptFeedback;
|
|
100
|
+
usageMetadata?: GeminiUsageMetadata;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface GeminiImageToolDetails {
|
|
104
|
+
model: string;
|
|
105
|
+
imageCount: number;
|
|
106
|
+
responseText?: string;
|
|
107
|
+
promptFeedback?: GeminiPromptFeedback;
|
|
108
|
+
usage?: GeminiUsageMetadata;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface ImageInput {
|
|
112
|
+
path?: string;
|
|
113
|
+
data?: string;
|
|
114
|
+
mime_type?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface InlineImageData {
|
|
118
|
+
data: string;
|
|
119
|
+
mimeType: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeDataUrl(data: string): { data: string; mimeType?: string } {
|
|
123
|
+
const match = data.match(/^data:([^;]+);base64,(.+)$/);
|
|
124
|
+
if (!match) return { data };
|
|
125
|
+
return { data: match[2] ?? "", mimeType: match[1] };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function findGeminiApiKey(): Promise<string | null> {
|
|
129
|
+
const geminiKey = await getEnv("GEMINI_API_KEY");
|
|
130
|
+
if (geminiKey) return geminiKey;
|
|
131
|
+
|
|
132
|
+
const googleKey = await getEnv("GOOGLE_API_KEY");
|
|
133
|
+
if (googleKey) return googleKey;
|
|
134
|
+
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function loadImageFromPath(imagePath: string, cwd: string): Promise<InlineImageData> {
|
|
139
|
+
const resolved = resolveReadPath(imagePath, cwd);
|
|
140
|
+
const file = Bun.file(resolved);
|
|
141
|
+
if (!(await file.exists())) {
|
|
142
|
+
throw new Error(`Image file not found: ${imagePath}`);
|
|
143
|
+
}
|
|
144
|
+
if (file.size > MAX_IMAGE_SIZE) {
|
|
145
|
+
throw new Error(`Image file too large: ${imagePath}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const mimeType = await detectSupportedImageMimeTypeFromFile(resolved);
|
|
149
|
+
if (!mimeType) {
|
|
150
|
+
throw new Error(`Unsupported image type: ${imagePath}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
154
|
+
return { data: buffer.toString("base64"), mimeType };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function resolveInputImage(input: ImageInput, cwd: string): Promise<InlineImageData> {
|
|
158
|
+
if (input.path) {
|
|
159
|
+
return loadImageFromPath(input.path, cwd);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (input.data) {
|
|
163
|
+
const normalized = normalizeDataUrl(input.data.trim());
|
|
164
|
+
const mimeType = normalized.mimeType ?? input.mime_type;
|
|
165
|
+
if (!mimeType) {
|
|
166
|
+
throw new Error("mime_type is required when providing raw base64 data.");
|
|
167
|
+
}
|
|
168
|
+
if (!normalized.data) {
|
|
169
|
+
throw new Error("Image data is empty.");
|
|
170
|
+
}
|
|
171
|
+
return { data: normalized.data, mimeType };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
throw new Error("input_images entries must include either path or data.");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function buildResponseSummary(model: string, imageCount: number, responseText: string | undefined): string {
|
|
178
|
+
const lines = [`Model: ${model}`, `Images: ${imageCount}`];
|
|
179
|
+
if (responseText) {
|
|
180
|
+
lines.push("", responseText.trim());
|
|
181
|
+
}
|
|
182
|
+
return lines.join("\n");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function collectResponseText(parts: GeminiPart[]): string | undefined {
|
|
186
|
+
const texts = parts.map((part) => part.text).filter((text): text is string => Boolean(text));
|
|
187
|
+
const combined = texts.join("\n").trim();
|
|
188
|
+
return combined.length > 0 ? combined : undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function collectInlineImages(parts: GeminiPart[]): InlineImageData[] {
|
|
192
|
+
const images: InlineImageData[] = [];
|
|
193
|
+
for (const part of parts) {
|
|
194
|
+
const data = part.inlineData?.data;
|
|
195
|
+
const mimeType = part.inlineData?.mimeType;
|
|
196
|
+
if (!data || !mimeType) continue;
|
|
197
|
+
images.push({ data, mimeType });
|
|
198
|
+
}
|
|
199
|
+
return images;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function combineParts(response: GeminiGenerateContentResponse): GeminiPart[] {
|
|
203
|
+
const parts: GeminiPart[] = [];
|
|
204
|
+
for (const candidate of response.candidates ?? []) {
|
|
205
|
+
const candidateParts = candidate.content?.parts ?? [];
|
|
206
|
+
parts.push(...candidateParts);
|
|
207
|
+
}
|
|
208
|
+
return parts;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function createAbortController(
|
|
212
|
+
signal: AbortSignal | undefined,
|
|
213
|
+
timeoutSeconds: number,
|
|
214
|
+
): { controller: AbortController; cleanup: () => void } {
|
|
215
|
+
const controller = new AbortController();
|
|
216
|
+
const timeout = setTimeout(() => controller.abort(), timeoutSeconds * 1000);
|
|
217
|
+
|
|
218
|
+
let abortListener: (() => void) | undefined;
|
|
219
|
+
if (signal) {
|
|
220
|
+
abortListener = () => controller.abort(signal.reason);
|
|
221
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const cleanup = () => {
|
|
225
|
+
clearTimeout(timeout);
|
|
226
|
+
if (abortListener && signal) {
|
|
227
|
+
signal.removeEventListener("abort", abortListener);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
return { controller, cleanup };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export const geminiImageTool: CustomTool<typeof geminiImageSchema, GeminiImageToolDetails> = {
|
|
235
|
+
name: "gemini_image",
|
|
236
|
+
label: "Gemini Image",
|
|
237
|
+
description: geminiImageDescription,
|
|
238
|
+
parameters: geminiImageSchema,
|
|
239
|
+
async execute(_toolCallId, params, _onUpdate, ctx, signal) {
|
|
240
|
+
return untilAborted(signal, async () => {
|
|
241
|
+
const apiKey = await findGeminiApiKey();
|
|
242
|
+
if (!apiKey) {
|
|
243
|
+
throw new Error("GEMINI_API_KEY not found.");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const model = params.model ?? DEFAULT_MODEL;
|
|
247
|
+
const responseModalities = params.response_modalities ?? ["Image"];
|
|
248
|
+
const cwd = ctx.sessionManager.getCwd();
|
|
249
|
+
|
|
250
|
+
const parts = [] as Array<{ text?: string; inlineData?: InlineImageData }>;
|
|
251
|
+
if (params.input_images?.length) {
|
|
252
|
+
for (const input of params.input_images) {
|
|
253
|
+
const image = await resolveInputImage(input, cwd);
|
|
254
|
+
parts.push({ inlineData: image });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
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
|
+
|
|
278
|
+
const timeoutSeconds = params.timeout_seconds ?? DEFAULT_TIMEOUT_SECONDS;
|
|
279
|
+
const { controller, cleanup } = createAbortController(signal, timeoutSeconds);
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const response = await fetch(
|
|
283
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`,
|
|
284
|
+
{
|
|
285
|
+
method: "POST",
|
|
286
|
+
headers: {
|
|
287
|
+
"Content-Type": "application/json",
|
|
288
|
+
"x-goog-api-key": apiKey,
|
|
289
|
+
},
|
|
290
|
+
body: JSON.stringify(requestBody),
|
|
291
|
+
signal: controller.signal,
|
|
292
|
+
},
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const rawText = await response.text();
|
|
296
|
+
if (!response.ok) {
|
|
297
|
+
let message = rawText;
|
|
298
|
+
try {
|
|
299
|
+
const parsed = JSON.parse(rawText) as { error?: { message?: string } };
|
|
300
|
+
message = parsed.error?.message ?? message;
|
|
301
|
+
} catch {
|
|
302
|
+
// Keep raw text.
|
|
303
|
+
}
|
|
304
|
+
throw new Error(`Gemini image request failed (${response.status}): ${message}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const data = JSON.parse(rawText) as GeminiGenerateContentResponse;
|
|
308
|
+
const responseParts = combineParts(data);
|
|
309
|
+
const responseText = collectResponseText(responseParts);
|
|
310
|
+
const inlineImages = collectInlineImages(responseParts);
|
|
311
|
+
const content: Array<TextContent | ImageContent> = [];
|
|
312
|
+
|
|
313
|
+
if (inlineImages.length === 0) {
|
|
314
|
+
const blocked = data.promptFeedback?.blockReason
|
|
315
|
+
? `Blocked: ${data.promptFeedback.blockReason}`
|
|
316
|
+
: "No image data returned.";
|
|
317
|
+
content.push({ type: "text", text: `${blocked}${responseText ? `\n\n${responseText}` : ""}` });
|
|
318
|
+
return {
|
|
319
|
+
content,
|
|
320
|
+
details: {
|
|
321
|
+
model,
|
|
322
|
+
imageCount: 0,
|
|
323
|
+
responseText,
|
|
324
|
+
promptFeedback: data.promptFeedback,
|
|
325
|
+
usage: data.usageMetadata,
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
content.push({
|
|
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
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
content,
|
|
340
|
+
details: {
|
|
341
|
+
model,
|
|
342
|
+
imageCount: inlineImages.length,
|
|
343
|
+
responseText,
|
|
344
|
+
promptFeedback: data.promptFeedback,
|
|
345
|
+
usage: data.usageMetadata,
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
} finally {
|
|
349
|
+
cleanup();
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
export async function getGeminiImageTools(): Promise<
|
|
356
|
+
Array<CustomTool<typeof geminiImageSchema, GeminiImageToolDetails>>
|
|
357
|
+
> {
|
|
358
|
+
const apiKey = await findGeminiApiKey();
|
|
359
|
+
if (!apiKey) return [];
|
|
360
|
+
return [geminiImageTool];
|
|
361
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { type GitParams, gitTool as gitToolCore, type ToolResponse } from "@oh-my-pi/pi-git-tool";
|
|
3
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
4
|
+
import gitDescription from "../../prompts/tools/git.md" with { type: "text" };
|
|
5
|
+
|
|
6
|
+
const gitSchema = Type.Object({
|
|
7
|
+
operation: Type.Union([
|
|
8
|
+
Type.Literal("status"),
|
|
9
|
+
Type.Literal("diff"),
|
|
10
|
+
Type.Literal("log"),
|
|
11
|
+
Type.Literal("show"),
|
|
12
|
+
Type.Literal("blame"),
|
|
13
|
+
Type.Literal("branch"),
|
|
14
|
+
Type.Literal("add"),
|
|
15
|
+
Type.Literal("restore"),
|
|
16
|
+
Type.Literal("commit"),
|
|
17
|
+
Type.Literal("checkout"),
|
|
18
|
+
Type.Literal("merge"),
|
|
19
|
+
Type.Literal("rebase"),
|
|
20
|
+
Type.Literal("stash"),
|
|
21
|
+
Type.Literal("cherry-pick"),
|
|
22
|
+
Type.Literal("fetch"),
|
|
23
|
+
Type.Literal("pull"),
|
|
24
|
+
Type.Literal("push"),
|
|
25
|
+
Type.Literal("tag"),
|
|
26
|
+
Type.Literal("pr"),
|
|
27
|
+
Type.Literal("issue"),
|
|
28
|
+
Type.Literal("ci"),
|
|
29
|
+
Type.Literal("release"),
|
|
30
|
+
]),
|
|
31
|
+
|
|
32
|
+
// Status
|
|
33
|
+
only: Type.Optional(
|
|
34
|
+
Type.Union([
|
|
35
|
+
Type.Literal("branch"),
|
|
36
|
+
Type.Literal("modified"),
|
|
37
|
+
Type.Literal("staged"),
|
|
38
|
+
Type.Literal("untracked"),
|
|
39
|
+
Type.Literal("conflicts"),
|
|
40
|
+
Type.Literal("sync"),
|
|
41
|
+
]),
|
|
42
|
+
),
|
|
43
|
+
ignored: Type.Optional(Type.Boolean()),
|
|
44
|
+
|
|
45
|
+
// Diff
|
|
46
|
+
target: Type.Optional(
|
|
47
|
+
Type.Union([
|
|
48
|
+
Type.Literal("unstaged"),
|
|
49
|
+
Type.Literal("staged"),
|
|
50
|
+
Type.Literal("head"),
|
|
51
|
+
Type.Object({
|
|
52
|
+
from: Type.String(),
|
|
53
|
+
to: Type.Optional(Type.String()),
|
|
54
|
+
}),
|
|
55
|
+
Type.String(),
|
|
56
|
+
]),
|
|
57
|
+
),
|
|
58
|
+
paths: Type.Optional(Type.Array(Type.String())),
|
|
59
|
+
stat_only: Type.Optional(Type.Boolean()),
|
|
60
|
+
name_only: Type.Optional(Type.Boolean()),
|
|
61
|
+
context: Type.Optional(Type.Number()),
|
|
62
|
+
max_lines: Type.Optional(Type.Number()),
|
|
63
|
+
ignore_whitespace: Type.Optional(Type.Boolean()),
|
|
64
|
+
|
|
65
|
+
// Log
|
|
66
|
+
limit: Type.Optional(Type.Number()),
|
|
67
|
+
ref: Type.Optional(Type.String()),
|
|
68
|
+
author: Type.Optional(Type.String()),
|
|
69
|
+
since: Type.Optional(Type.String()),
|
|
70
|
+
until: Type.Optional(Type.String()),
|
|
71
|
+
grep: Type.Optional(Type.String()),
|
|
72
|
+
format: Type.Optional(Type.Union([Type.Literal("oneline"), Type.Literal("short"), Type.Literal("full")])),
|
|
73
|
+
stat: Type.Optional(Type.Boolean()),
|
|
74
|
+
merges: Type.Optional(Type.Boolean()),
|
|
75
|
+
first_parent: Type.Optional(Type.Boolean()),
|
|
76
|
+
|
|
77
|
+
// Show
|
|
78
|
+
path: Type.Optional(Type.String()),
|
|
79
|
+
diff: Type.Optional(Type.Boolean()),
|
|
80
|
+
lines: Type.Optional(
|
|
81
|
+
Type.Object({
|
|
82
|
+
start: Type.Number(),
|
|
83
|
+
end: Type.Number(),
|
|
84
|
+
}),
|
|
85
|
+
),
|
|
86
|
+
|
|
87
|
+
// Blame
|
|
88
|
+
root: Type.Optional(Type.Boolean()),
|
|
89
|
+
|
|
90
|
+
// Branch
|
|
91
|
+
action: Type.Optional(
|
|
92
|
+
Type.Union([
|
|
93
|
+
Type.Literal("list"),
|
|
94
|
+
Type.Literal("create"),
|
|
95
|
+
Type.Literal("delete"),
|
|
96
|
+
Type.Literal("rename"),
|
|
97
|
+
Type.Literal("current"),
|
|
98
|
+
]),
|
|
99
|
+
),
|
|
100
|
+
name: Type.Optional(Type.String()),
|
|
101
|
+
newName: Type.Optional(Type.String()),
|
|
102
|
+
startPoint: Type.Optional(Type.String()),
|
|
103
|
+
remotes: Type.Optional(Type.Boolean()),
|
|
104
|
+
force: Type.Optional(Type.Boolean()),
|
|
105
|
+
|
|
106
|
+
// Add/Restore
|
|
107
|
+
update: Type.Optional(Type.Boolean()),
|
|
108
|
+
all: Type.Optional(Type.Boolean()),
|
|
109
|
+
dry_run: Type.Optional(Type.Boolean()),
|
|
110
|
+
staged: Type.Optional(Type.Boolean()),
|
|
111
|
+
worktree: Type.Optional(Type.Boolean()),
|
|
112
|
+
source: Type.Optional(Type.String()),
|
|
113
|
+
|
|
114
|
+
// Commit
|
|
115
|
+
message: Type.Optional(Type.String()),
|
|
116
|
+
allow_empty: Type.Optional(Type.Boolean()),
|
|
117
|
+
sign: Type.Optional(Type.Boolean()),
|
|
118
|
+
no_verify: Type.Optional(Type.Boolean()),
|
|
119
|
+
amend: Type.Optional(Type.Boolean()),
|
|
120
|
+
|
|
121
|
+
// Checkout
|
|
122
|
+
create: Type.Optional(Type.Boolean()),
|
|
123
|
+
|
|
124
|
+
// Merge
|
|
125
|
+
no_ff: Type.Optional(Type.Boolean()),
|
|
126
|
+
ff_only: Type.Optional(Type.Boolean()),
|
|
127
|
+
squash: Type.Optional(Type.Boolean()),
|
|
128
|
+
abort: Type.Optional(Type.Boolean()),
|
|
129
|
+
continue: Type.Optional(Type.Boolean()),
|
|
130
|
+
|
|
131
|
+
// Rebase
|
|
132
|
+
onto: Type.Optional(Type.String()),
|
|
133
|
+
upstream: Type.Optional(Type.String()),
|
|
134
|
+
skip: Type.Optional(Type.Boolean()),
|
|
135
|
+
|
|
136
|
+
// Stash
|
|
137
|
+
include_untracked: Type.Optional(Type.Boolean()),
|
|
138
|
+
index: Type.Optional(Type.Number()),
|
|
139
|
+
keep_index: Type.Optional(Type.Boolean()),
|
|
140
|
+
|
|
141
|
+
// Cherry-pick
|
|
142
|
+
commits: Type.Optional(Type.Array(Type.String())),
|
|
143
|
+
no_commit: Type.Optional(Type.Boolean()),
|
|
144
|
+
|
|
145
|
+
// Fetch/Pull/Push/Tag
|
|
146
|
+
remote: Type.Optional(Type.String()),
|
|
147
|
+
branch: Type.Optional(Type.String()),
|
|
148
|
+
prune: Type.Optional(Type.Boolean()),
|
|
149
|
+
tags: Type.Optional(Type.Boolean()),
|
|
150
|
+
rebase: Type.Optional(Type.Boolean()),
|
|
151
|
+
set_upstream: Type.Optional(Type.Boolean()),
|
|
152
|
+
force_with_lease: Type.Optional(Type.Boolean()),
|
|
153
|
+
delete: Type.Optional(Type.Boolean()),
|
|
154
|
+
force_override: Type.Optional(Type.Boolean()),
|
|
155
|
+
|
|
156
|
+
// Tag
|
|
157
|
+
// (name/message/ref already covered)
|
|
158
|
+
|
|
159
|
+
// PR
|
|
160
|
+
number: Type.Optional(Type.Number()),
|
|
161
|
+
title: Type.Optional(Type.String()),
|
|
162
|
+
body: Type.Optional(Type.String()),
|
|
163
|
+
base: Type.Optional(Type.String()),
|
|
164
|
+
head: Type.Optional(Type.String()),
|
|
165
|
+
draft: Type.Optional(Type.Boolean()),
|
|
166
|
+
state: Type.Optional(
|
|
167
|
+
Type.Union([Type.Literal("open"), Type.Literal("closed"), Type.Literal("merged"), Type.Literal("all")]),
|
|
168
|
+
),
|
|
169
|
+
merge_method: Type.Optional(Type.Union([Type.Literal("merge"), Type.Literal("squash"), Type.Literal("rebase")])),
|
|
170
|
+
review_action: Type.Optional(
|
|
171
|
+
Type.Union([Type.Literal("approve"), Type.Literal("request-changes"), Type.Literal("comment")]),
|
|
172
|
+
),
|
|
173
|
+
review_body: Type.Optional(Type.String()),
|
|
174
|
+
|
|
175
|
+
// Issue
|
|
176
|
+
labels: Type.Optional(Type.Array(Type.String())),
|
|
177
|
+
assignee: Type.Optional(Type.String()),
|
|
178
|
+
comment_body: Type.Optional(Type.String()),
|
|
179
|
+
|
|
180
|
+
// CI
|
|
181
|
+
workflow: Type.Optional(Type.String()),
|
|
182
|
+
run_id: Type.Optional(Type.Number()),
|
|
183
|
+
inputs: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
184
|
+
logs_failed: Type.Optional(Type.Boolean()),
|
|
185
|
+
|
|
186
|
+
// Release
|
|
187
|
+
notes: Type.Optional(Type.String()),
|
|
188
|
+
generate_notes: Type.Optional(Type.Boolean()),
|
|
189
|
+
prerelease: Type.Optional(Type.Boolean()),
|
|
190
|
+
assets: Type.Optional(Type.Array(Type.String())),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
export type GitToolDetails = ToolResponse<unknown>;
|
|
194
|
+
|
|
195
|
+
export function createGitTool(cwd: string): AgentTool<typeof gitSchema, GitToolDetails> {
|
|
196
|
+
return {
|
|
197
|
+
name: "git",
|
|
198
|
+
label: "Git",
|
|
199
|
+
description: gitDescription,
|
|
200
|
+
parameters: gitSchema,
|
|
201
|
+
execute: async (_toolCallId, params: Static<typeof gitSchema>, _signal?: AbortSignal) => {
|
|
202
|
+
const result = await gitToolCore(params as GitParams, cwd);
|
|
203
|
+
if ("error" in result) {
|
|
204
|
+
const message = result._rendered ?? result.error;
|
|
205
|
+
return { content: [{ type: "text", text: message }], details: result };
|
|
206
|
+
}
|
|
207
|
+
if ("confirm" in result) {
|
|
208
|
+
const message = result._rendered ?? result.confirm;
|
|
209
|
+
return { content: [{ type: "text", text: message }], details: result };
|
|
210
|
+
}
|
|
211
|
+
return { content: [{ type: "text", text: result._rendered }], details: result };
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export const gitTool = createGitTool(process.cwd());
|