@99percentpeople/pi-codex-api 0.1.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/LICENSE +21 -0
- package/README.md +207 -0
- package/client.ts +237 -0
- package/config.ts +79 -0
- package/image.ts +343 -0
- package/index.ts +103 -0
- package/package.json +64 -0
- package/render.ts +23 -0
- package/search-display.ts +259 -0
- package/search.ts +316 -0
- package/settings.ts +122 -0
- package/skills/gpt-image-prompts/SKILL.md +282 -0
- package/usage.ts +628 -0
package/image.ts
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, extname, isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import type { ImageContent } from "@earendil-works/pi-ai";
|
|
4
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
import { createCodexApiClient } from "./client.ts";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_CODEX_API_CONFIG,
|
|
9
|
+
type CodexApiConfig,
|
|
10
|
+
type CodexImageQuality,
|
|
11
|
+
} from "./config.ts";
|
|
12
|
+
import {
|
|
13
|
+
reusableText,
|
|
14
|
+
streamingSuffix,
|
|
15
|
+
textOutput,
|
|
16
|
+
} from "./render.ts";
|
|
17
|
+
|
|
18
|
+
const IMAGE_MODEL = "gpt-image-2";
|
|
19
|
+
const MAX_REFERENCE_IMAGES = 5;
|
|
20
|
+
const MIN_IMAGE_PIXELS = 655_360;
|
|
21
|
+
const MAX_IMAGE_PIXELS = 8_294_400;
|
|
22
|
+
const MAX_IMAGE_EDGE = 3_840;
|
|
23
|
+
|
|
24
|
+
const ImageQualitySchema = Type.Union([
|
|
25
|
+
Type.Literal("auto"),
|
|
26
|
+
Type.Literal("low"),
|
|
27
|
+
Type.Literal("medium"),
|
|
28
|
+
Type.Literal("high"),
|
|
29
|
+
], {
|
|
30
|
+
description: "Per-call quality override. Omit to use the /99settings default; override only when the user explicitly asks for a draft or quality level",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const IMAGE_MIME_TYPES: Record<string, string> = {
|
|
34
|
+
".gif": "image/gif",
|
|
35
|
+
".jpeg": "image/jpeg",
|
|
36
|
+
".jpg": "image/jpeg",
|
|
37
|
+
".png": "image/png",
|
|
38
|
+
".webp": "image/webp",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type CodexImagePhase =
|
|
42
|
+
| "preparing"
|
|
43
|
+
| "authenticating"
|
|
44
|
+
| "reading-references"
|
|
45
|
+
| "generating"
|
|
46
|
+
| "saving"
|
|
47
|
+
| "completed";
|
|
48
|
+
|
|
49
|
+
export interface CodexImageDetails {
|
|
50
|
+
phase: CodexImagePhase;
|
|
51
|
+
savedPath?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface ImageResponse {
|
|
55
|
+
data?: Array<{ b64_json?: unknown }>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface ImageReference {
|
|
59
|
+
image_url: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sanitizeFilePart(value: string): string {
|
|
63
|
+
const sanitized = value.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
64
|
+
return sanitized || "generated_image";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function normalizeCodexImageSize(value?: string): string {
|
|
68
|
+
const normalized = value?.trim().toLowerCase() || "auto";
|
|
69
|
+
if (normalized === "auto") return normalized;
|
|
70
|
+
const match = /^([1-9]\d*)x([1-9]\d*)$/.exec(normalized);
|
|
71
|
+
if (!match) {
|
|
72
|
+
throw new Error("Image size must be auto or WIDTHxHEIGHT, for example 1536x1024");
|
|
73
|
+
}
|
|
74
|
+
const width = Number(match[1]);
|
|
75
|
+
const height = Number(match[2]);
|
|
76
|
+
const pixels = width * height;
|
|
77
|
+
if (width % 16 !== 0 || height % 16 !== 0) {
|
|
78
|
+
throw new Error("GPT Image 2 width and height must both be divisible by 16");
|
|
79
|
+
}
|
|
80
|
+
if (width > MAX_IMAGE_EDGE || height > MAX_IMAGE_EDGE) {
|
|
81
|
+
throw new Error(`GPT Image 2 width and height must not exceed ${MAX_IMAGE_EDGE}px`);
|
|
82
|
+
}
|
|
83
|
+
if (Math.max(width, height) / Math.min(width, height) > 3) {
|
|
84
|
+
throw new Error("GPT Image 2 aspect ratio must be between 1:3 and 3:1");
|
|
85
|
+
}
|
|
86
|
+
if (pixels < MIN_IMAGE_PIXELS || pixels > MAX_IMAGE_PIXELS) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`GPT Image 2 size must contain between ${MIN_IMAGE_PIXELS.toLocaleString("en-US")} and ${MAX_IMAGE_PIXELS.toLocaleString("en-US")} pixels`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return `${width}x${height}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function workspacePath(cwd: string, path: string): string {
|
|
95
|
+
const root = resolve(cwd);
|
|
96
|
+
const absolute = isAbsolute(path) ? resolve(path) : resolve(root, path);
|
|
97
|
+
const fromRoot = relative(root, absolute);
|
|
98
|
+
if (fromRoot === ".." || fromRoot.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(fromRoot)) {
|
|
99
|
+
throw new Error(`Image path must stay inside the current workspace: ${path}`);
|
|
100
|
+
}
|
|
101
|
+
return absolute;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function outputPath(cwd: string, toolCallId: string, requested?: string): string {
|
|
105
|
+
const path = requested?.trim()
|
|
106
|
+
? requested.trim()
|
|
107
|
+
: `output/codex-images/${sanitizeFilePart(toolCallId)}.png`;
|
|
108
|
+
const absolute = workspacePath(cwd, path);
|
|
109
|
+
return extname(absolute).toLowerCase() === ".png" ? absolute : `${absolute}.png`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function assertDoesNotExist(path: string): Promise<void> {
|
|
113
|
+
try {
|
|
114
|
+
await access(path);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
throw new Error(`Refusing to overwrite existing image: ${path}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function imageDataUrl(cwd: string, path: string): Promise<string> {
|
|
123
|
+
const absolute = workspacePath(cwd, path);
|
|
124
|
+
const mimeType = IMAGE_MIME_TYPES[extname(absolute).toLowerCase()];
|
|
125
|
+
if (!mimeType) throw new Error(`Unsupported reference image type: ${path}`);
|
|
126
|
+
const bytes = await readFile(absolute);
|
|
127
|
+
return `data:${mimeType};base64,${bytes.toString("base64")}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function conversationImageDataUrl(value: ImageContent): string | undefined {
|
|
131
|
+
if (!value.data || !value.mimeType.toLowerCase().startsWith("image/")) return undefined;
|
|
132
|
+
if (value.data.startsWith("data:image/")) return value.data;
|
|
133
|
+
const mimeType = value.mimeType.toLowerCase() === "image/jpg" ? "image/jpeg" : value.mimeType;
|
|
134
|
+
return `data:${mimeType};base64,${value.data}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function imagesFromContent(content: unknown): string[] {
|
|
138
|
+
if (!Array.isArray(content)) return [];
|
|
139
|
+
const images: string[] = [];
|
|
140
|
+
for (const item of content) {
|
|
141
|
+
if (!item || typeof item !== "object" || (item as { type?: unknown }).type !== "image") continue;
|
|
142
|
+
const dataUrl = conversationImageDataUrl(item as ImageContent);
|
|
143
|
+
if (dataUrl) images.push(dataUrl);
|
|
144
|
+
}
|
|
145
|
+
return images;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function recentConversationImages(ctx: ExtensionContext, count: number): ImageReference[] {
|
|
149
|
+
const images: string[] = [];
|
|
150
|
+
for (const entry of ctx.sessionManager.buildContextEntries()) {
|
|
151
|
+
if (entry.type === "message" && "content" in entry.message) {
|
|
152
|
+
images.push(...imagesFromContent(entry.message.content));
|
|
153
|
+
} else if (entry.type === "custom_message") {
|
|
154
|
+
images.push(...imagesFromContent(entry.content));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const selected = images.slice(-count);
|
|
158
|
+
if (selected.length !== count) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Requested the last ${count} conversation image${count === 1 ? "" : "s"}, but only ${selected.length} were available`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return selected.map((image_url) => ({ image_url }));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function firstImage(response: ImageResponse): string {
|
|
167
|
+
const value = response.data?.[0]?.b64_json;
|
|
168
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
169
|
+
throw new Error("Codex image API returned no image data");
|
|
170
|
+
}
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function imagePhaseLabel(phase: CodexImagePhase): string {
|
|
175
|
+
if (phase === "preparing") return "Preparing image request…";
|
|
176
|
+
if (phase === "authenticating") return "Authenticating with Codex…";
|
|
177
|
+
if (phase === "reading-references") return "Reading reference images…";
|
|
178
|
+
if (phase === "generating") return "Waiting for Codex image generation…";
|
|
179
|
+
if (phase === "saving") return "Saving generated PNG…";
|
|
180
|
+
return "Image completed";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function registerCodexImageTool(
|
|
184
|
+
pi: ExtensionAPI,
|
|
185
|
+
getConfig: () => CodexApiConfig = () => DEFAULT_CODEX_API_CONFIG,
|
|
186
|
+
refreshUsageInBackground?: (ctx: ExtensionContext) => void,
|
|
187
|
+
): void {
|
|
188
|
+
pi.registerTool({
|
|
189
|
+
name: "codex_image",
|
|
190
|
+
label: "Codex Image",
|
|
191
|
+
description:
|
|
192
|
+
"Generate a PNG with the Codex subscription image API, or edit with up to five local or recent conversation images. Uses the active openai-codex OAuth subscription and gpt-image-2; no API key is required.",
|
|
193
|
+
promptSnippet: "Generate or edit raster images through the active Codex subscription",
|
|
194
|
+
promptGuidelines: [
|
|
195
|
+
"Use codex_image for requested raster images, illustrations, mockups, textures, or edits when the active model uses openai-codex OAuth, or Other providers is enabled in /99settings and Codex OAuth is logged in.",
|
|
196
|
+
"For a new image, omit both reference fields. For an edit, use referenced_image_paths for local files or num_last_images_to_include for recent attached/generated conversation images; never provide both.",
|
|
197
|
+
"Omit size and quality unless the user explicitly requests exact dimensions, a draft, or a quality level; otherwise official automatic sizing and the user's /99settings quality default apply.",
|
|
198
|
+
"Use a new output_path and do not overwrite an existing asset; report the saved path after generation.",
|
|
199
|
+
],
|
|
200
|
+
parameters: Type.Object({
|
|
201
|
+
prompt: Type.String({
|
|
202
|
+
minLength: 1,
|
|
203
|
+
description: "Detailed image generation or editing prompt",
|
|
204
|
+
}),
|
|
205
|
+
referenced_image_paths: Type.Optional(Type.Array(Type.String({ minLength: 1 }), {
|
|
206
|
+
maxItems: MAX_REFERENCE_IMAGES,
|
|
207
|
+
description: "Local PNG, JPEG, WebP, or GIF paths used for an edit",
|
|
208
|
+
})),
|
|
209
|
+
num_last_images_to_include: Type.Optional(Type.Integer({
|
|
210
|
+
minimum: 1,
|
|
211
|
+
maximum: MAX_REFERENCE_IMAGES,
|
|
212
|
+
description: "Use the smallest number of recent attached or generated conversation images needed for an edit; do not combine with referenced_image_paths",
|
|
213
|
+
})),
|
|
214
|
+
size: Type.Optional(Type.String({
|
|
215
|
+
minLength: 1,
|
|
216
|
+
pattern: "^(auto|[1-9][0-9]*x[1-9][0-9]*)$",
|
|
217
|
+
description: "Exact GPT Image 2 output size as WIDTHxHEIGHT only when required. Edges must be divisible by 16 and at most 3840px, aspect ratio 1:3 to 3:1, total 655360 to 8294400 pixels",
|
|
218
|
+
})),
|
|
219
|
+
quality: Type.Optional(ImageQualitySchema),
|
|
220
|
+
output_path: Type.Optional(Type.String({
|
|
221
|
+
minLength: 1,
|
|
222
|
+
description: "Destination PNG path; defaults under output/codex-images",
|
|
223
|
+
})),
|
|
224
|
+
}, { additionalProperties: false }),
|
|
225
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
226
|
+
const references = params.referenced_image_paths ?? [];
|
|
227
|
+
const recentImageCount = params.num_last_images_to_include;
|
|
228
|
+
if (references.length > MAX_REFERENCE_IMAGES) {
|
|
229
|
+
throw new Error(`referenced_image_paths accepts at most ${MAX_REFERENCE_IMAGES} images`);
|
|
230
|
+
}
|
|
231
|
+
if (references.length > 0 && recentImageCount !== undefined) {
|
|
232
|
+
throw new Error("Provide only one of referenced_image_paths or num_last_images_to_include");
|
|
233
|
+
}
|
|
234
|
+
const operation = references.length === 0 && recentImageCount === undefined ? "generate" : "edit";
|
|
235
|
+
const savedPath = outputPath(ctx.cwd, toolCallId, params.output_path);
|
|
236
|
+
const config = getConfig();
|
|
237
|
+
const quality: CodexImageQuality = params.quality ?? config.imageQuality ?? "auto";
|
|
238
|
+
const size = normalizeCodexImageSize(params.size);
|
|
239
|
+
const update = (phase: CodexImagePhase) => onUpdate?.({
|
|
240
|
+
content: [{ type: "text", text: imagePhaseLabel(phase) }],
|
|
241
|
+
details: {
|
|
242
|
+
phase,
|
|
243
|
+
savedPath,
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
update("preparing");
|
|
247
|
+
await assertDoesNotExist(savedPath);
|
|
248
|
+
update("authenticating");
|
|
249
|
+
const client = await createCodexApiClient(ctx, {
|
|
250
|
+
allowOtherProviders: config.allowOtherProviders,
|
|
251
|
+
});
|
|
252
|
+
let images: ImageReference[] | undefined;
|
|
253
|
+
if (references.length > 0) {
|
|
254
|
+
update("reading-references");
|
|
255
|
+
images = await Promise.all(
|
|
256
|
+
references.map(async (path) => ({ image_url: await imageDataUrl(ctx.cwd, path) })),
|
|
257
|
+
);
|
|
258
|
+
} else if (recentImageCount !== undefined) {
|
|
259
|
+
update("reading-references");
|
|
260
|
+
images = recentConversationImages(ctx, recentImageCount);
|
|
261
|
+
}
|
|
262
|
+
const request = {
|
|
263
|
+
prompt: params.prompt,
|
|
264
|
+
background: "auto",
|
|
265
|
+
model: IMAGE_MODEL,
|
|
266
|
+
quality,
|
|
267
|
+
size,
|
|
268
|
+
};
|
|
269
|
+
update("generating");
|
|
270
|
+
const response = images === undefined
|
|
271
|
+
? await client.post<ImageResponse>("images/generations", request, signal)
|
|
272
|
+
: await client.post<ImageResponse>("images/edits", { ...request, images }, signal);
|
|
273
|
+
const data = firstImage(response);
|
|
274
|
+
update("saving");
|
|
275
|
+
await mkdir(dirname(savedPath), { recursive: true });
|
|
276
|
+
await writeFile(savedPath, Buffer.from(data, "base64"));
|
|
277
|
+
refreshUsageInBackground?.(ctx);
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
content: [
|
|
281
|
+
{ type: "text", text: `${operation === "edit" ? "Edited" : "Generated"} image saved to ${savedPath}` },
|
|
282
|
+
{ type: "image", data, mimeType: "image/png" },
|
|
283
|
+
],
|
|
284
|
+
details: {
|
|
285
|
+
phase: "completed",
|
|
286
|
+
savedPath,
|
|
287
|
+
} satisfies CodexImageDetails,
|
|
288
|
+
};
|
|
289
|
+
},
|
|
290
|
+
renderCall(args, theme, context) {
|
|
291
|
+
const text = reusableText(context);
|
|
292
|
+
const references = Array.isArray(args.referenced_image_paths)
|
|
293
|
+
? args.referenced_image_paths
|
|
294
|
+
: [];
|
|
295
|
+
const recentImageCount = typeof args.num_last_images_to_include === "number"
|
|
296
|
+
? args.num_last_images_to_include
|
|
297
|
+
: undefined;
|
|
298
|
+
const operation = references.length > 0 || recentImageCount !== undefined ? "edit" : "generate";
|
|
299
|
+
const prompt = typeof args.prompt === "string" && args.prompt
|
|
300
|
+
? JSON.stringify(args.prompt)
|
|
301
|
+
: "";
|
|
302
|
+
const referencePaths = references.filter((path): path is string => typeof path === "string");
|
|
303
|
+
const referenceParameter = referencePaths.length > 0
|
|
304
|
+
? `references=[${referencePaths.map((path) => JSON.stringify(path)).join(", ")}]`
|
|
305
|
+
: "";
|
|
306
|
+
const recentParameter = recentImageCount !== undefined ? `recent=${recentImageCount}` : "";
|
|
307
|
+
const sizeParameter = typeof args.size === "string" && args.size ? `size=${args.size}` : "";
|
|
308
|
+
const qualityParameter = typeof args.quality === "string" && args.quality
|
|
309
|
+
? `quality=${args.quality}`
|
|
310
|
+
: "";
|
|
311
|
+
const outputParameter = typeof args.output_path === "string" && args.output_path
|
|
312
|
+
? `output=${JSON.stringify(args.output_path)}`
|
|
313
|
+
: "";
|
|
314
|
+
text.setText(
|
|
315
|
+
theme.fg("toolTitle", theme.bold("codex_image"))
|
|
316
|
+
+ (operation ? ` ${theme.fg("accent", operation)}` : "")
|
|
317
|
+
+ (prompt ? ` ${theme.fg("muted", prompt)}` : "")
|
|
318
|
+
+ (referenceParameter ? ` ${theme.fg("dim", referenceParameter)}` : "")
|
|
319
|
+
+ (recentParameter ? ` ${theme.fg("dim", recentParameter)}` : "")
|
|
320
|
+
+ (sizeParameter ? ` ${theme.fg("dim", sizeParameter)}` : "")
|
|
321
|
+
+ (qualityParameter ? ` ${theme.fg("dim", qualityParameter)}` : "")
|
|
322
|
+
+ (outputParameter ? ` ${theme.fg("muted", outputParameter)}` : "")
|
|
323
|
+
+ streamingSuffix(theme, context.argsComplete || context.executionStarted),
|
|
324
|
+
);
|
|
325
|
+
return text;
|
|
326
|
+
},
|
|
327
|
+
renderResult(result, { isPartial }, theme, context) {
|
|
328
|
+
const text = reusableText(context);
|
|
329
|
+
const details = result.details as CodexImageDetails | undefined;
|
|
330
|
+
const output = textOutput(result.content);
|
|
331
|
+
if (isPartial) {
|
|
332
|
+
text.setText(theme.fg("warning", imagePhaseLabel(details?.phase ?? "preparing")));
|
|
333
|
+
return text;
|
|
334
|
+
}
|
|
335
|
+
if (context.isError || !details) {
|
|
336
|
+
text.setText(output ? theme.fg("error", output) : theme.fg("error", "Codex image request failed"));
|
|
337
|
+
return text;
|
|
338
|
+
}
|
|
339
|
+
text.setText(output ? theme.fg("toolOutput", output) : "");
|
|
340
|
+
return text;
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
loadCodexApiConfig,
|
|
4
|
+
saveCodexApiConfig,
|
|
5
|
+
type CodexApiConfig,
|
|
6
|
+
} from "./config.ts";
|
|
7
|
+
import { registerCodexImageTool } from "./image.ts";
|
|
8
|
+
import { registerCodexSearchTool } from "./search.ts";
|
|
9
|
+
import { registerCodexApiSettings } from "./settings.ts";
|
|
10
|
+
import {
|
|
11
|
+
registerCodexUsageAndFast,
|
|
12
|
+
type CodexUsageHandle,
|
|
13
|
+
} from "./usage.ts";
|
|
14
|
+
|
|
15
|
+
export default function (pi: ExtensionAPI) {
|
|
16
|
+
let config = loadCodexApiConfig();
|
|
17
|
+
let usageHandle: CodexUsageHandle | undefined;
|
|
18
|
+
|
|
19
|
+
const controller = {
|
|
20
|
+
getConfig: () => config,
|
|
21
|
+
updateConfig: (next: CodexApiConfig, ctx: Parameters<CodexUsageHandle["refreshStatus"]>[0]) => {
|
|
22
|
+
config = next;
|
|
23
|
+
try {
|
|
24
|
+
saveCodexApiConfig(config);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
ctx.ui.notify(
|
|
27
|
+
`Failed to save Codex API settings: ${error instanceof Error ? error.message : String(error)}`,
|
|
28
|
+
"error",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
usageHandle?.refreshStatus(ctx);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
usageHandle = registerCodexUsageAndFast(pi, controller);
|
|
36
|
+
const refreshUsageInBackground = (ctx: Parameters<CodexUsageHandle["refreshUsage"]>[0]) => {
|
|
37
|
+
void usageHandle?.refreshUsage(ctx).catch(() => {});
|
|
38
|
+
};
|
|
39
|
+
registerCodexImageTool(pi, () => config, refreshUsageInBackground);
|
|
40
|
+
registerCodexSearchTool(pi, () => config, refreshUsageInBackground);
|
|
41
|
+
registerCodexApiSettings(pi, controller);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
CodexApiClient,
|
|
46
|
+
CodexApiError,
|
|
47
|
+
createCodexApiClient,
|
|
48
|
+
extractCodexAccountId,
|
|
49
|
+
resolveCodexApiRoot,
|
|
50
|
+
type CodexApiClientContextOptions,
|
|
51
|
+
type CodexApiClientOptions,
|
|
52
|
+
type CodexFetch,
|
|
53
|
+
} from "./client.ts";
|
|
54
|
+
export {
|
|
55
|
+
CODEX_API_SETTINGS_NAMESPACE,
|
|
56
|
+
DEFAULT_CODEX_API_CONFIG,
|
|
57
|
+
getCodexApiConfigPath,
|
|
58
|
+
loadCodexApiConfig,
|
|
59
|
+
normalizeCodexApiConfig,
|
|
60
|
+
saveCodexApiConfig,
|
|
61
|
+
type CodexApiConfig,
|
|
62
|
+
type CodexImageQuality,
|
|
63
|
+
type CodexSearchContextSize,
|
|
64
|
+
type CodexSearchMode,
|
|
65
|
+
} from "./config.ts";
|
|
66
|
+
export {
|
|
67
|
+
normalizeCodexImageSize,
|
|
68
|
+
registerCodexImageTool,
|
|
69
|
+
type CodexImageDetails,
|
|
70
|
+
} from "./image.ts";
|
|
71
|
+
export {
|
|
72
|
+
cleanCodexSearchOutput,
|
|
73
|
+
createCodexSearchDisplay,
|
|
74
|
+
formatCodexSearchDisplay,
|
|
75
|
+
type CodexSearchDisplay,
|
|
76
|
+
type CodexSearchDisplayLine,
|
|
77
|
+
type CodexSearchDisplayLineRole,
|
|
78
|
+
type CodexSearchSource,
|
|
79
|
+
} from "./search-display.ts";
|
|
80
|
+
export {
|
|
81
|
+
registerCodexSearchTool,
|
|
82
|
+
SearchCommandsSchema,
|
|
83
|
+
type CodexSearchDetails,
|
|
84
|
+
} from "./search.ts";
|
|
85
|
+
export {
|
|
86
|
+
CONTEXT_SIZE_LABELS,
|
|
87
|
+
IMAGE_QUALITY_LABELS,
|
|
88
|
+
registerCodexApiSettings,
|
|
89
|
+
SEARCH_MODE_LABELS,
|
|
90
|
+
} from "./settings.ts";
|
|
91
|
+
export {
|
|
92
|
+
applyFastModePayload,
|
|
93
|
+
formatCodexStatus,
|
|
94
|
+
formatCodexUsage,
|
|
95
|
+
parseCodexRateLimits,
|
|
96
|
+
parseCodexUsagePayload,
|
|
97
|
+
registerCodexUsageAndFast,
|
|
98
|
+
type CodexCreditsSnapshot,
|
|
99
|
+
type CodexRateLimitSnapshot,
|
|
100
|
+
type CodexRateLimitWindow,
|
|
101
|
+
type CodexUsageHandle,
|
|
102
|
+
type CodexUsageOptions,
|
|
103
|
+
} from "./usage.ts";
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@99percentpeople/pi-codex-api",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Expose Codex subscription image, search, Fast, and usage APIs as native Pi tools and commands",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.ts",
|
|
8
|
+
"client.ts",
|
|
9
|
+
"config.ts",
|
|
10
|
+
"image.ts",
|
|
11
|
+
"render.ts",
|
|
12
|
+
"search-display.ts",
|
|
13
|
+
"search.ts",
|
|
14
|
+
"settings.ts",
|
|
15
|
+
"usage.ts",
|
|
16
|
+
"skills",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"prepublishOnly": "bun run --cwd ../.. check"
|
|
22
|
+
},
|
|
23
|
+
"pi": {
|
|
24
|
+
"extensions": [
|
|
25
|
+
"./index.ts"
|
|
26
|
+
],
|
|
27
|
+
"skills": [
|
|
28
|
+
"./skills"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"pi-package",
|
|
33
|
+
"pi",
|
|
34
|
+
"coding-agent",
|
|
35
|
+
"codex",
|
|
36
|
+
"chatgpt",
|
|
37
|
+
"image-generation",
|
|
38
|
+
"web-search"
|
|
39
|
+
],
|
|
40
|
+
"author": "99percentpeople",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/99percentpeople/pi-extensions.git",
|
|
45
|
+
"directory": "extensions/codex-api"
|
|
46
|
+
},
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/99percentpeople/pi-extensions/issues"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/99percentpeople/pi-extensions#codex-api",
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public",
|
|
53
|
+
"registry": "https://registry.npmjs.org/"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@99percentpeople/pi-shared-settings": "0.1.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"@earendil-works/pi-ai": "*",
|
|
60
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
61
|
+
"@earendil-works/pi-tui": "*",
|
|
62
|
+
"typebox": "*"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/render.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Text, type Component } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
interface ReusableRenderContext {
|
|
5
|
+
lastComponent: Component | undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function reusableText(context: ReusableRenderContext): Text {
|
|
9
|
+
return context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function streamingSuffix(theme: Theme, argsComplete: boolean): string {
|
|
13
|
+
return argsComplete ? "" : theme.fg("dim", " …");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function textOutput(content: Array<{ type: string; text?: string }>): string {
|
|
17
|
+
return content
|
|
18
|
+
.filter((item): item is { type: "text"; text: string } =>
|
|
19
|
+
item.type === "text" && typeof item.text === "string"
|
|
20
|
+
)
|
|
21
|
+
.map((item) => item.text)
|
|
22
|
+
.join("\n");
|
|
23
|
+
}
|