@oai404iao/pi-codex-minimal-tools 1.4.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -17
- package/THIRD_PARTY_NOTICES.md +20 -0
- package/config.schema.json +1 -2
- package/index.ts +2 -0
- package/package.json +14 -13
- package/provenance/openai-codex-ddea03ad-astra.json +54 -0
- package/src/activation.ts +1 -56
- package/src/adapter/compaction/checkpoint.ts +1 -0
- package/src/adapter/compaction/collect.ts +1 -0
- package/src/adapter/compaction/http.ts +1 -0
- package/src/adapter/compaction/request.ts +1 -0
- package/src/adapter/compaction/transport.ts +1 -0
- package/src/adapter/compaction/websocket.ts +1 -0
- package/src/background-image-generation.ts +1 -574
- package/src/capabilities.ts +1 -146
- package/src/codex-http.ts +1 -133
- package/src/codex-identity-extension.ts +1 -339
- package/src/codex-request-profile.ts +1 -45
- package/src/codex-reserved-tools.ts +1 -323
- package/src/codex-wire-identity.ts +1 -596
- package/src/extension/prewarm-snapshot.ts +1 -0
- package/src/extension/provider-presentation.ts +1 -0
- package/src/extension/provider-runtime.ts +1 -0
- package/src/extension/register.ts +36 -0
- package/src/extension/startup-prewarm.ts +1 -0
- package/src/fast-mode.ts +1 -124
- package/src/glyphs.ts +1 -70
- package/src/index.ts +8 -331
- package/src/model-catalog/catalog.ts +1 -636
- package/src/model-catalog/default-models.json +8 -0
- package/src/model-catalog/runtime.ts +1 -113
- package/src/model-catalog/types.ts +1 -95
- package/src/native-compaction.ts +1 -393
- package/src/patch/apply.ts +1 -338
- package/src/patch/parser.ts +1 -224
- package/src/patch/render.ts +1 -201
- package/src/provider-headers.ts +1 -54
- package/src/provider-native-tools.ts +1 -71
- package/src/provider-shim.ts +20 -4511
- package/src/providers/codex-apply-patch-tool.ts +1 -23
- package/src/providers/openai-codex/cache-key.ts +1 -0
- package/src/providers/openai-codex/captured-stream.ts +1 -0
- package/src/providers/openai-codex/constants.ts +1 -0
- package/src/providers/openai-codex/continuation.ts +1 -0
- package/src/providers/openai-codex/errors.ts +1 -0
- package/src/providers/openai-codex/events.ts +1 -0
- package/src/providers/openai-codex/headers.ts +1 -0
- package/src/providers/openai-codex/lite.ts +1 -0
- package/src/providers/openai-codex/message.ts +1 -0
- package/src/providers/openai-codex/prewarm.ts +1 -0
- package/src/providers/openai-codex/proxy.ts +1 -0
- package/src/providers/openai-codex/reasoning.ts +1 -0
- package/src/providers/openai-codex/request-body.ts +1 -0
- package/src/providers/openai-codex/request-context.ts +1 -0
- package/src/providers/openai-codex/request-metadata.ts +1 -0
- package/src/providers/openai-codex/retry.ts +1 -0
- package/src/providers/openai-codex/runtime.ts +1 -0
- package/src/providers/openai-codex/sse.ts +1 -0
- package/src/providers/openai-codex/stream-effects.ts +1 -0
- package/src/providers/openai-codex/stream.ts +1 -0
- package/src/providers/openai-codex/types.ts +1 -0
- package/src/providers/openai-codex/urls.ts +1 -0
- package/src/providers/openai-codex/usage.ts +1 -0
- package/src/providers/openai-codex/websocket-connection.ts +1 -0
- package/src/providers/openai-codex/websocket-events.ts +1 -0
- package/src/providers/openai-codex/websocket-session.ts +1 -0
- package/src/providers/openai-codex/websocket-socket.ts +1 -0
- package/src/providers/openai-codex/websocket-stream.ts +1 -0
- package/src/providers/openai-responses-shared.ts +7 -1463
- package/src/providers/responses/citations.ts +1 -0
- package/src/providers/responses/history.ts +1 -0
- package/src/providers/responses/items.ts +1 -0
- package/src/providers/responses/markdown.ts +1 -0
- package/src/providers/responses/messages.ts +1 -0
- package/src/providers/responses/signatures.ts +1 -0
- package/src/providers/responses/stream-state.ts +1 -0
- package/src/providers/responses/stream.ts +1 -0
- package/src/providers/responses/text-renderer.ts +1 -0
- package/src/providers/responses/text.ts +1 -0
- package/src/providers/responses/tool-identity.ts +1 -0
- package/src/providers/responses/tools.ts +1 -0
- package/src/providers/responses/types.ts +1 -0
- package/src/providers/responses/usage.ts +1 -0
- package/src/settings.ts +1 -247
- package/src/subagent-inline.ts +1 -8
- package/src/tools/apply-patch.ts +1 -84
- package/src/tools/image-generation/capture.ts +1 -0
- package/src/tools/image-generation/display.ts +1 -0
- package/src/tools/image-generation/preview.ts +1 -0
- package/src/tools/image-generation/storage.ts +1 -0
- package/src/tools/image-generation/types.ts +1 -0
- package/src/tools/image-generation.ts +1 -274
- package/src/tools/view-image.ts +1 -98
- package/src/tools/web-search/activity.ts +1 -0
- package/src/tools/web-search/capture.ts +1 -0
- package/src/tools/web-search/render.ts +1 -0
- package/src/tools/web-search.ts +1 -554
- package/src/utils/images.ts +1 -73
- package/src/utils/theme.ts +1 -0
|
@@ -1,274 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { extname, isAbsolute, resolve } from "node:path";
|
|
4
|
-
import { buildSessionContext, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { saveBase64Image } from "../utils/images.js";
|
|
6
|
-
import type { CodexMinimalToolsSettings } from "../settings.js";
|
|
7
|
-
import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
|
|
8
|
-
import {
|
|
9
|
-
buildCodexJsonHeaders,
|
|
10
|
-
hasCodexRequestAuth,
|
|
11
|
-
resolveCodexApiEndpoint,
|
|
12
|
-
} from "../codex-http.js";
|
|
13
|
-
import { loadModelSettings, type ResolvedCodexModelSettings } from "../model-catalog/runtime.js";
|
|
14
|
-
|
|
15
|
-
export interface ImageGenerationInput {
|
|
16
|
-
prompt?: string;
|
|
17
|
-
referenced_image_paths?: string[];
|
|
18
|
-
num_last_images_to_include?: number;
|
|
19
|
-
size?: "1024x1024" | "1024x1536" | "1536x1024" | "auto";
|
|
20
|
-
quality?: "low" | "medium" | "high" | "auto";
|
|
21
|
-
background?: "transparent" | "opaque" | "auto";
|
|
22
|
-
output_format?: "png" | "webp" | "jpeg";
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const imageGenerationToolSchema = {
|
|
26
|
-
type: "object",
|
|
27
|
-
additionalProperties: false,
|
|
28
|
-
properties: {
|
|
29
|
-
prompt: { type: "string", description: "Image generation or editing prompt." },
|
|
30
|
-
referenced_image_paths: {
|
|
31
|
-
type: "array",
|
|
32
|
-
maxItems: 5,
|
|
33
|
-
items: { type: "string", minLength: 1 },
|
|
34
|
-
description: "Local PNG, JPEG, or WebP paths to edit.",
|
|
35
|
-
},
|
|
36
|
-
num_last_images_to_include: {
|
|
37
|
-
type: "integer",
|
|
38
|
-
minimum: 1,
|
|
39
|
-
maximum: 5,
|
|
40
|
-
description: "Use the newest conversation images when one or more targets have no local path.",
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
required: ["prompt"],
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
async function urlToBase64(url: string, signal?: AbortSignal): Promise<string> {
|
|
47
|
-
const response = await fetch(url, { signal });
|
|
48
|
-
if (!response.ok) throw new Error(`Failed to download generated image: ${response.status} ${await response.text()}`);
|
|
49
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
50
|
-
return buffer.toString("base64");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function directImageGeneration(input: ImageGenerationInput, cwd: string, settings: CodexMinimalToolsSettings, signal?: AbortSignal) {
|
|
54
|
-
if (!settings.directImageApiFallback) throw new Error("Direct Images API fallback is disabled. Use native openai or openai-codex handling, or enable directImageApiFallback.");
|
|
55
|
-
const apiKey = process.env.OPENAI_API_KEY;
|
|
56
|
-
if (!apiKey) throw new Error("OPENAI_API_KEY is required for direct image_generation fallback.");
|
|
57
|
-
if (!input.prompt?.trim()) throw new Error("A prompt is required for direct image_generation fallback.");
|
|
58
|
-
const body: Record<string, unknown> = {
|
|
59
|
-
model: settings.imageModel,
|
|
60
|
-
prompt: input.prompt,
|
|
61
|
-
};
|
|
62
|
-
if (input.size && input.size !== "auto") body.size = input.size;
|
|
63
|
-
if (input.quality && input.quality !== "auto") body.quality = input.quality;
|
|
64
|
-
if (input.background && input.background !== "auto") body.background = input.background;
|
|
65
|
-
if (input.output_format) body.output_format = input.output_format;
|
|
66
|
-
const response = await fetch("https://api.openai.com/v1/images/generations", {
|
|
67
|
-
method: "POST",
|
|
68
|
-
headers: { Authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
|
|
69
|
-
body: JSON.stringify(body),
|
|
70
|
-
signal,
|
|
71
|
-
});
|
|
72
|
-
if (!response.ok) throw new Error(`OpenAI Images API failed: ${response.status} ${await response.text()}`);
|
|
73
|
-
const json = await response.json() as { data?: Array<{ b64_json?: string; url?: string; revised_prompt?: string }> };
|
|
74
|
-
const first = json.data?.[0];
|
|
75
|
-
const base64 = first?.b64_json ?? (first?.url ? await urlToBase64(first.url, signal) : undefined);
|
|
76
|
-
if (!base64) throw new Error("OpenAI Images API returned no image data.");
|
|
77
|
-
const saved = await saveBase64Image({ base64, callId: "direct", cwd, format: input.output_format, responseId: settings.imageModel, settings });
|
|
78
|
-
return {
|
|
79
|
-
content: [{ type: "text", text: `Generated image with ${settings.imageModel}; saved to ${saved.path}${saved.latestPath ? ` (latest: ${saved.latestPath})` : ""}.` }],
|
|
80
|
-
details: { saved, revisedPrompt: first?.revised_prompt, mode: "direct-images-api" },
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
interface ImageGenerationToolContext {
|
|
85
|
-
cwd: string;
|
|
86
|
-
model?: Model<Api>;
|
|
87
|
-
modelRegistry?: {
|
|
88
|
-
getApiKeyAndHeaders(model: Model<Api>): Promise<
|
|
89
|
-
| { ok: true; apiKey?: string; headers?: ProviderHeaders }
|
|
90
|
-
| { ok: false; error: string }
|
|
91
|
-
>;
|
|
92
|
-
};
|
|
93
|
-
sessionManager?: {
|
|
94
|
-
getSessionId?(): string;
|
|
95
|
-
getBranch(): SessionEntry[];
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export interface StandaloneImageGenerationInvocation {
|
|
100
|
-
callId?: string;
|
|
101
|
-
turnId?: string;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function referencedImageUrls(cwd: string, paths: readonly string[]): Promise<Array<{ image_url: string }>> {
|
|
105
|
-
if (paths.length > 5) throw new Error("referenced_image_paths must contain at most 5 paths.");
|
|
106
|
-
return Promise.all(paths.map(async (rawPath) => {
|
|
107
|
-
const normalized = rawPath.replace(/^@/, "");
|
|
108
|
-
const path = isAbsolute(normalized) ? normalized : resolve(cwd, normalized);
|
|
109
|
-
const extension = extname(path).toLowerCase();
|
|
110
|
-
const mimeType = extension === ".png"
|
|
111
|
-
? "image/png"
|
|
112
|
-
: extension === ".jpg" || extension === ".jpeg"
|
|
113
|
-
? "image/jpeg"
|
|
114
|
-
: extension === ".webp"
|
|
115
|
-
? "image/webp"
|
|
116
|
-
: undefined;
|
|
117
|
-
if (!mimeType) throw new Error(`Unsupported reference image type: ${rawPath}. Use PNG, JPEG, or WebP.`);
|
|
118
|
-
const data = await readFile(path);
|
|
119
|
-
return { image_url: `data:${mimeType};base64,${data.toString("base64")}` };
|
|
120
|
-
}));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function recentConversationImageUrls(
|
|
124
|
-
ctx: ImageGenerationToolContext,
|
|
125
|
-
count: number,
|
|
126
|
-
): Array<{ image_url: string }> {
|
|
127
|
-
if (!ctx.sessionManager) {
|
|
128
|
-
throw new Error("Conversation images are unavailable in this tool context; use referenced_image_paths.");
|
|
129
|
-
}
|
|
130
|
-
const messages = buildSessionContext(ctx.sessionManager.getBranch()).messages;
|
|
131
|
-
const images: string[] = [];
|
|
132
|
-
for (const message of messages) {
|
|
133
|
-
if (message.role === "user" || message.role === "toolResult") {
|
|
134
|
-
if (!Array.isArray(message.content)) continue;
|
|
135
|
-
for (const item of message.content) {
|
|
136
|
-
if (item.type === "image") {
|
|
137
|
-
images.push(`data:${item.mimeType};base64,${item.data}`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
if (message.role !== "assistant") continue;
|
|
143
|
-
for (const block of message.content as unknown[]) {
|
|
144
|
-
if (!block || typeof block !== "object") continue;
|
|
145
|
-
const candidate = block as {
|
|
146
|
-
type?: unknown;
|
|
147
|
-
item?: { result?: unknown };
|
|
148
|
-
};
|
|
149
|
-
if (
|
|
150
|
-
candidate.type === "image_generation_call"
|
|
151
|
-
&& typeof candidate.item?.result === "string"
|
|
152
|
-
&& candidate.item.result
|
|
153
|
-
) {
|
|
154
|
-
images.push(`data:image/png;base64,${candidate.item.result}`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
if (images.length < count) {
|
|
159
|
-
throw new Error(`Requested the last ${count} conversation images, but only ${images.length} were available.`);
|
|
160
|
-
}
|
|
161
|
-
return images.slice(-count).map((image_url) => ({ image_url }));
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export async function standaloneImageGeneration(
|
|
165
|
-
input: ImageGenerationInput,
|
|
166
|
-
ctx: ImageGenerationToolContext,
|
|
167
|
-
settings: ResolvedCodexModelSettings,
|
|
168
|
-
signal?: AbortSignal,
|
|
169
|
-
invocation: StandaloneImageGenerationInvocation = {},
|
|
170
|
-
) {
|
|
171
|
-
const callId = invocation.callId ?? "standalone";
|
|
172
|
-
const turnId = invocation.turnId ?? randomUUID();
|
|
173
|
-
const model = ctx.model;
|
|
174
|
-
if (!model || !ctx.modelRegistry) throw new Error("No active model is available for standalone image generation.");
|
|
175
|
-
if (!settings.enabled) throw new Error("pi-codex-minimal-tools is disabled.");
|
|
176
|
-
if (!input.prompt?.trim()) throw new Error("A prompt is required for standalone image generation.");
|
|
177
|
-
if (input.num_last_images_to_include !== undefined && input.referenced_image_paths?.length) {
|
|
178
|
-
throw new Error("Provide only one of referenced_image_paths or num_last_images_to_include.");
|
|
179
|
-
}
|
|
180
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
181
|
-
if (!auth.ok) throw new Error(auth.error);
|
|
182
|
-
if (!hasCodexRequestAuth({
|
|
183
|
-
modelHeaders: model.headers,
|
|
184
|
-
auth: { apiKey: auth.apiKey, headers: auth.headers },
|
|
185
|
-
})) {
|
|
186
|
-
throw new Error(`No request authentication for provider: ${model.provider}`);
|
|
187
|
-
}
|
|
188
|
-
const images = input.num_last_images_to_include !== undefined
|
|
189
|
-
? recentConversationImageUrls(ctx, input.num_last_images_to_include)
|
|
190
|
-
: await referencedImageUrls(ctx.cwd, input.referenced_image_paths ?? []);
|
|
191
|
-
const edit = images.length > 0;
|
|
192
|
-
const response = await fetch(
|
|
193
|
-
resolveCodexApiEndpoint(
|
|
194
|
-
model.baseUrl,
|
|
195
|
-
settings.apiKeyMode,
|
|
196
|
-
edit ? "images/edits" : "images/generations",
|
|
197
|
-
),
|
|
198
|
-
{
|
|
199
|
-
method: "POST",
|
|
200
|
-
headers: buildCodexJsonHeaders({
|
|
201
|
-
modelHeaders: model.headers,
|
|
202
|
-
auth: { apiKey: auth.apiKey, headers: auth.headers },
|
|
203
|
-
apiKeyMode: settings.apiKeyMode,
|
|
204
|
-
extraHeaders: {
|
|
205
|
-
"x-codex-image-turn-id": turnId,
|
|
206
|
-
},
|
|
207
|
-
}),
|
|
208
|
-
body: JSON.stringify({
|
|
209
|
-
model: settings.imageModel,
|
|
210
|
-
prompt: input.prompt.trim(),
|
|
211
|
-
...(edit ? { images } : {}),
|
|
212
|
-
background: "auto",
|
|
213
|
-
quality: "auto",
|
|
214
|
-
size: "auto",
|
|
215
|
-
}),
|
|
216
|
-
signal,
|
|
217
|
-
},
|
|
218
|
-
);
|
|
219
|
-
if (!response.ok) {
|
|
220
|
-
throw new Error(`Standalone image generation failed: HTTP ${response.status}: ${await response.text()}`);
|
|
221
|
-
}
|
|
222
|
-
const result = await response.json() as {
|
|
223
|
-
data?: Array<{ b64_json?: string }>;
|
|
224
|
-
};
|
|
225
|
-
const base64 = result.data?.[0]?.b64_json;
|
|
226
|
-
if (!base64) throw new Error("Standalone image generation returned no image data.");
|
|
227
|
-
const saved = await saveBase64Image({
|
|
228
|
-
base64,
|
|
229
|
-
callId,
|
|
230
|
-
cwd: ctx.cwd,
|
|
231
|
-
format: "png",
|
|
232
|
-
responseId: settings.imageModel,
|
|
233
|
-
settings,
|
|
234
|
-
});
|
|
235
|
-
return {
|
|
236
|
-
content: [
|
|
237
|
-
{ type: "image", data: base64, mimeType: "image/png" },
|
|
238
|
-
{ type: "text", text: `Generated image with ${settings.imageModel}; saved to ${saved.path}${saved.latestPath ? ` (latest: ${saved.latestPath})` : ""}.` },
|
|
239
|
-
],
|
|
240
|
-
details: { saved, mode: "standalone-images-api" },
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
export function createImageGenerationToolDefinition(options: {
|
|
245
|
-
loadSettings?: (cwd: string, model?: Model<Api>) => CodexMinimalToolsSettings | ResolvedCodexModelSettings;
|
|
246
|
-
getCurrentTurnId?: (sessionId: string | undefined) => string | undefined;
|
|
247
|
-
} = {}) {
|
|
248
|
-
return {
|
|
249
|
-
name: "image_generation",
|
|
250
|
-
label: "Image Generation",
|
|
251
|
-
description: "Generate or edit images using the hosted or standalone implementation selected by the current model profile. Results are saved under imageOutputDir and mirrored to latest.<ext>.",
|
|
252
|
-
promptSnippet: "Generate or edit images with the implementation selected by the current model profile.",
|
|
253
|
-
parameters: imageGenerationToolSchema,
|
|
254
|
-
async execute(toolCallId: string, params: ImageGenerationInput, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: ImageGenerationToolContext) {
|
|
255
|
-
const cwd = ctx?.cwd ?? process.cwd();
|
|
256
|
-
const settings = options.loadSettings?.(cwd, ctx.model) ?? loadModelSettings(ctx.model, cwd);
|
|
257
|
-
const resolvedSettings = "modelProfile" in settings
|
|
258
|
-
? settings as ResolvedCodexModelSettings
|
|
259
|
-
: loadModelSettings(ctx.model, cwd, settings);
|
|
260
|
-
if (resolvedSettings.imageGenerationImplementation === "standalone") {
|
|
261
|
-
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
262
|
-
return standaloneImageGeneration(params, ctx, resolvedSettings, signal, {
|
|
263
|
-
callId: toolCallId,
|
|
264
|
-
turnId: options.getCurrentTurnId?.(sessionId),
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
if (settings?.directImageApiFallback) return directImageGeneration(params, cwd, settings, signal);
|
|
268
|
-
return {
|
|
269
|
-
content: [{ type: "text", text: "image_generation should be handled by the current model profile. If no hosted or standalone implementation is configured, enable directImageApiFallback with OPENAI_API_KEY." }],
|
|
270
|
-
details: { phase: "native-provider", nativeTool: "image_generation" },
|
|
271
|
-
};
|
|
272
|
-
},
|
|
273
|
-
};
|
|
274
|
-
}
|
|
1
|
+
export * from "@oai404iao/pi-codex-imagegen/internal/tools/image-generation";
|
package/src/tools/view-image.ts
CHANGED
|
@@ -1,98 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { extname, isAbsolute, relative, resolve } from "node:path";
|
|
3
|
-
|
|
4
|
-
export type ImageDetail = "auto" | "low" | "high" | "original";
|
|
5
|
-
|
|
6
|
-
export interface ViewImageInput {
|
|
7
|
-
path: string;
|
|
8
|
-
detail?: ImageDetail;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface ViewImageOptions {
|
|
12
|
-
workspaceOnly?: boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface ValidatedImage {
|
|
16
|
-
absolutePath: string;
|
|
17
|
-
displayPath: string;
|
|
18
|
-
mimeType: string;
|
|
19
|
-
sizeBytes: number;
|
|
20
|
-
detail: ImageDetail;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const IMAGE_MIME_BY_EXT: Record<string, string> = {
|
|
24
|
-
".png": "image/png",
|
|
25
|
-
".jpg": "image/jpeg",
|
|
26
|
-
".jpeg": "image/jpeg",
|
|
27
|
-
".gif": "image/gif",
|
|
28
|
-
".webp": "image/webp",
|
|
29
|
-
".bmp": "image/bmp",
|
|
30
|
-
".tif": "image/tiff",
|
|
31
|
-
".tiff": "image/tiff",
|
|
32
|
-
".svg": "image/svg+xml",
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
function assertWithinCwd(absolutePath: string, cwd: string, displayPath: string): void {
|
|
36
|
-
const cwdAbsolute = resolve(cwd);
|
|
37
|
-
const rel = relative(cwdAbsolute, absolutePath);
|
|
38
|
-
if (rel === "" || (!rel.startsWith("..") && !isAbsolute(rel))) return;
|
|
39
|
-
throw new Error(`view_image path escapes the workspace: ${displayPath}`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function normalizeImagePath(pathValue: string, cwd: string, options?: ViewImageOptions): { absolutePath: string; displayPath: string } {
|
|
43
|
-
let cleaned = pathValue.trim();
|
|
44
|
-
if ((cleaned.startsWith('"') && cleaned.endsWith('"')) || (cleaned.startsWith("'") && cleaned.endsWith("'"))) cleaned = cleaned.slice(1, -1);
|
|
45
|
-
if (cleaned.startsWith("@")) cleaned = cleaned.slice(1);
|
|
46
|
-
const absolutePath = resolve(cwd, cleaned);
|
|
47
|
-
if (options?.workspaceOnly) assertWithinCwd(absolutePath, cwd, cleaned);
|
|
48
|
-
return { absolutePath, displayPath: cleaned };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function mimeTypeForImagePath(path: string): string | undefined {
|
|
52
|
-
return IMAGE_MIME_BY_EXT[extname(path).toLowerCase()];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export async function validateImagePath(input: ViewImageInput, cwd: string, options?: ViewImageOptions): Promise<ValidatedImage> {
|
|
56
|
-
if (!input || typeof input.path !== "string" || input.path.trim().length === 0) throw new Error("view_image requires a non-empty path.");
|
|
57
|
-
const detail = input.detail ?? "auto";
|
|
58
|
-
if (!["auto", "low", "high", "original"].includes(detail)) throw new Error(`Unsupported image detail: ${String(input.detail)}`);
|
|
59
|
-
const normalized = normalizeImagePath(input.path, cwd, options);
|
|
60
|
-
let fileStat;
|
|
61
|
-
try {
|
|
62
|
-
fileStat = await stat(normalized.absolutePath);
|
|
63
|
-
} catch {
|
|
64
|
-
throw new Error(`Image not found: ${normalized.displayPath}`);
|
|
65
|
-
}
|
|
66
|
-
if (fileStat.isDirectory()) throw new Error(`view_image expected a file but got a directory: ${normalized.displayPath}`);
|
|
67
|
-
if (!fileStat.isFile()) throw new Error(`view_image expected a regular image file: ${normalized.displayPath}`);
|
|
68
|
-
if (options?.workspaceOnly) {
|
|
69
|
-
try {
|
|
70
|
-
assertWithinCwd(await realpath(normalized.absolutePath), await realpath(cwd), normalized.displayPath);
|
|
71
|
-
} catch (error) {
|
|
72
|
-
if (error instanceof Error && error.message.includes("escapes the workspace")) throw error;
|
|
73
|
-
throw new Error(`Unable to validate image path: ${normalized.displayPath}`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
const mimeType = mimeTypeForImagePath(normalized.absolutePath);
|
|
77
|
-
if (!mimeType) throw new Error(`Unsupported image file type for view_image: ${normalized.displayPath}`);
|
|
78
|
-
return { ...normalized, detail, mimeType, sizeBytes: fileStat.size };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export async function viewImage(input: ViewImageInput, cwd: string, options?: ViewImageOptions) {
|
|
82
|
-
const image = await validateImagePath(input, cwd, options);
|
|
83
|
-
const data = await readFile(image.absolutePath, "base64");
|
|
84
|
-
return {
|
|
85
|
-
content: [{ type: "image", data, mimeType: image.mimeType, detail: image.detail }],
|
|
86
|
-
details: image,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export const viewImageToolSchema = {
|
|
91
|
-
type: "object",
|
|
92
|
-
additionalProperties: false,
|
|
93
|
-
properties: {
|
|
94
|
-
path: { type: "string", description: "Path to the local image file. Relative paths resolve against ctx.cwd; a leading @ is accepted and stripped." },
|
|
95
|
-
detail: { type: "string", enum: ["auto", "low", "high", "original"], description: "Image detail hint. Defaults to auto." },
|
|
96
|
-
},
|
|
97
|
-
required: ["path"],
|
|
98
|
-
};
|
|
1
|
+
export * from "@oai404iao/pi-codex-core/internal/tools/view-image";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@oai404iao/pi-codex-web-search/internal/tools/web-search/activity";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@oai404iao/pi-codex-web-search/internal/tools/web-search/capture";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@oai404iao/pi-codex-web-search/internal/tools/web-search/render";
|