pi-better-openai 0.1.3 → 0.1.6
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 +13 -27
- package/extensions/image.ts +94 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,40 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
A pi extension for OpenAI subscription workflows: fast mode, usage visibility, footer polish, and image generation through `openai-codex` auth.
|
|
4
4
|
|
|
5
|
-
## Screenshots
|
|
6
|
-
|
|
7
|
-
<!-- Add screenshots here. -->
|
|
8
|
-
|
|
9
5
|
## Install
|
|
10
6
|
|
|
11
|
-
Install from
|
|
7
|
+
Install from GitHub:
|
|
12
8
|
|
|
13
9
|
```bash
|
|
14
|
-
pi install /
|
|
10
|
+
pi install github:mattleong/pi-better-openai
|
|
15
11
|
```
|
|
16
12
|
|
|
17
|
-
|
|
13
|
+
Or install from npm:
|
|
18
14
|
|
|
19
15
|
```bash
|
|
20
|
-
pi install
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Reload pi after installing:
|
|
24
|
-
|
|
25
|
-
```text
|
|
26
|
-
/reload
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Sign in to OpenAI Codex subscription auth if you want usage stats or image generation:
|
|
30
|
-
|
|
31
|
-
```text
|
|
32
|
-
/login openai-codex
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Start pi with fast mode enabled:
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
pi --fast
|
|
16
|
+
pi install npm:pi-better-openai
|
|
39
17
|
```
|
|
40
18
|
|
|
41
19
|
## Features
|
|
@@ -44,8 +22,16 @@ pi --fast
|
|
|
44
22
|
- OpenAI subscription usage display via `/openai-usage` and the footer.
|
|
45
23
|
- Interactive settings picker via `/openai-settings`.
|
|
46
24
|
- Footer customization for model, thinking, fast mode, usage, and token/cost context.
|
|
47
|
-
- OpenAI image generation/editing through the `openai_image` tool.
|
|
25
|
+
- OpenAI image generation/editing through the `openai_image` tool and `/openai-image` command.
|
|
48
26
|
- Commands:
|
|
49
27
|
- `/fast` toggles fast mode.
|
|
28
|
+
- `/openai-image <prompt>` generates an image directly.
|
|
50
29
|
- `/openai-usage` shows current OpenAI subscription usage.
|
|
51
30
|
- `/openai-settings` opens settings, diagnostics, and config details.
|
|
31
|
+
|
|
32
|
+
## Screenshots
|
|
33
|
+
|
|
34
|
+
<!-- Add screenshots here. -->
|
|
35
|
+
|
|
36
|
+
<img width="983" height="851" alt="Screenshot 2026-04-29 at 11 53 23 PM" src="https://github.com/user-attachments/assets/07a2fb87-ef48-4396-8b12-124825c8d360" />
|
|
37
|
+
<img width="1327" height="102" alt="Screenshot 2026-04-29 at 11 34 49 PM" src="https://github.com/user-attachments/assets/22042782-c94e-491d-b5af-095f7f0810f9" />
|
package/extensions/image.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { extname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { extname, isAbsolute, join, resolve, sep } from "node:path";
|
|
5
5
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
6
6
|
import type { ResolvedConfig } from "./config.ts";
|
|
7
7
|
import { isRecord } from "./config.ts";
|
|
8
8
|
import { readCodexAuth } from "./usage.ts";
|
|
9
9
|
|
|
10
10
|
const OPENAI_IMAGE_TOOL = "openai_image";
|
|
11
|
+
const OPENAI_IMAGE_COMMAND = "openai-image";
|
|
11
12
|
const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
12
13
|
const DEFAULT_TIMEOUT_MS = 180_000;
|
|
13
14
|
|
|
@@ -22,7 +23,7 @@ export type ImageOutputFormat = typeof IMAGE_OUTPUT_FORMATS[number];
|
|
|
22
23
|
const TOOL_PARAMS = {
|
|
23
24
|
type: "object",
|
|
24
25
|
properties: {
|
|
25
|
-
prompt: { type: "string", description: "Image generation/editing prompt." },
|
|
26
|
+
prompt: { type: "string", description: "Image generation/editing prompt. Pass the user's wording verbatim unless they explicitly ask you to refine or expand it." },
|
|
26
27
|
action: { type: "string", enum: IMAGE_ACTIONS, description: "Whether to generate a new image, edit/reference provided images, or let the model decide." },
|
|
27
28
|
images: {
|
|
28
29
|
type: "array",
|
|
@@ -337,6 +338,39 @@ async function requestCodexImage(params: ToolParams, ctx: ExtensionContext, cfg:
|
|
|
337
338
|
return { ...parsed, prompt: params.prompt, savedPath, model, action, outputFormat };
|
|
338
339
|
}
|
|
339
340
|
|
|
341
|
+
function displayPath(path: string): string {
|
|
342
|
+
const home = homedir();
|
|
343
|
+
if (!home) return path;
|
|
344
|
+
if (path === home) return "~";
|
|
345
|
+
const homePrefix = home.endsWith(sep) ? home : `${home}${sep}`;
|
|
346
|
+
return path.startsWith(homePrefix) ? `~/${path.slice(homePrefix.length)}` : path;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function textFromMessageContent(content: unknown): string | undefined {
|
|
350
|
+
if (typeof content === "string") return content.trim() || undefined;
|
|
351
|
+
if (!Array.isArray(content)) return undefined;
|
|
352
|
+
const text = content
|
|
353
|
+
.filter((part) => isRecord(part) && part.type === "text" && typeof part.text === "string")
|
|
354
|
+
.map((part) => (part as { text: string }).text)
|
|
355
|
+
.join("\n")
|
|
356
|
+
.trim();
|
|
357
|
+
return text || undefined;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function latestUserPromptFromEntries(entries: unknown[]): string | undefined {
|
|
361
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
362
|
+
const entry = entries[i];
|
|
363
|
+
if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message) || entry.message.role !== "user") continue;
|
|
364
|
+
const text = textFromMessageContent(entry.message.content);
|
|
365
|
+
if (text) return text;
|
|
366
|
+
}
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function resolveToolPrompt(params: ToolParams, ctx: ExtensionContext): string {
|
|
371
|
+
return latestUserPromptFromEntries(ctx.sessionManager.getEntries()) ?? params.prompt;
|
|
372
|
+
}
|
|
373
|
+
|
|
340
374
|
function resultText(result: CodexImageResult): string {
|
|
341
375
|
const parts = [
|
|
342
376
|
`Generated image via openai-codex/${result.model}.`,
|
|
@@ -344,7 +378,7 @@ function resultText(result: CodexImageResult): string {
|
|
|
344
378
|
`Prompt: ${result.prompt}`
|
|
345
379
|
];
|
|
346
380
|
if (result.revisedPrompt) parts.push(`Revised prompt: ${result.revisedPrompt}`);
|
|
347
|
-
if (result.savedPath) parts.push(`Saved: ${result.savedPath}`);
|
|
381
|
+
if (result.savedPath) parts.push(`Saved: ${displayPath(result.savedPath)}`);
|
|
348
382
|
return parts.join("\n");
|
|
349
383
|
}
|
|
350
384
|
|
|
@@ -387,6 +421,57 @@ export function registerOpenAIImage(pi: ExtensionAPI, getConfig: (ctx: Extension
|
|
|
387
421
|
};
|
|
388
422
|
}
|
|
389
423
|
|
|
424
|
+
void import("@mariozechner/pi-tui").then(({ Box, Container, Image, Text }) => {
|
|
425
|
+
pi.registerMessageRenderer<CodexImageResult>("openai-image", (message, _options, theme) => {
|
|
426
|
+
const result = message.details;
|
|
427
|
+
const text = result && isRecord(result)
|
|
428
|
+
? resultText(result as CodexImageResult)
|
|
429
|
+
: typeof message.content === "string"
|
|
430
|
+
? message.content
|
|
431
|
+
: message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
|
|
432
|
+
const image = result && isRecord(result) && typeof result.data === "string" && typeof result.mimeType === "string"
|
|
433
|
+
? { data: result.data, mimeType: result.mimeType, savedPath: typeof result.savedPath === "string" ? result.savedPath : undefined }
|
|
434
|
+
: Array.isArray(message.content)
|
|
435
|
+
? message.content.find((part) => part.type === "image" && typeof part.data === "string" && typeof part.mimeType === "string")
|
|
436
|
+
: undefined;
|
|
437
|
+
|
|
438
|
+
const container = new Container();
|
|
439
|
+
const box = new Box(1, 1, (line) => theme.bg("customMessageBg", line));
|
|
440
|
+
box.addChild(new Text(`${theme.fg("accent", theme.bold("[openai-image]"))}\n\n${text}`, 0, 0));
|
|
441
|
+
if (image) {
|
|
442
|
+
box.addChild(new Image(image.data, image.mimeType, { fallbackColor: (line) => theme.fg("dim", line) }, {
|
|
443
|
+
maxWidthCells: 80,
|
|
444
|
+
maxHeightCells: 24,
|
|
445
|
+
filename: "savedPath" in image && typeof image.savedPath === "string" ? image.savedPath : undefined
|
|
446
|
+
}));
|
|
447
|
+
}
|
|
448
|
+
container.addChild(box);
|
|
449
|
+
return container;
|
|
450
|
+
});
|
|
451
|
+
}).catch(() => undefined);
|
|
452
|
+
|
|
453
|
+
pi.registerCommand(OPENAI_IMAGE_COMMAND, {
|
|
454
|
+
description: "Generate an image with OpenAI Codex image generation",
|
|
455
|
+
handler: async (args, ctx) => {
|
|
456
|
+
const prompt = args.trim();
|
|
457
|
+
if (!prompt) {
|
|
458
|
+
ctx.ui.notify("Usage: /openai-image <prompt>", "error");
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
ctx.ui.notify("Requesting OpenAI image...", "info");
|
|
462
|
+
const result = await generate({ prompt }, ctx);
|
|
463
|
+
pi.sendMessage({
|
|
464
|
+
customType: "openai-image",
|
|
465
|
+
content: [
|
|
466
|
+
{ type: "text", text: resultText(result) },
|
|
467
|
+
{ type: "image", data: result.data, mimeType: result.mimeType }
|
|
468
|
+
],
|
|
469
|
+
display: true,
|
|
470
|
+
details: result
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
390
475
|
pi.registerTool({
|
|
391
476
|
name: OPENAI_IMAGE_TOOL,
|
|
392
477
|
label: "OpenAI image",
|
|
@@ -394,14 +479,16 @@ export function registerOpenAIImage(pi: ExtensionAPI, getConfig: (ctx: Extension
|
|
|
394
479
|
promptSnippet: "Generate or edit raster images via OpenAI Codex subscription auth.",
|
|
395
480
|
promptGuidelines: [
|
|
396
481
|
"Use openai_image when the user asks to generate or edit a raster image, photo, illustration, mockup, texture, sprite, or bitmap asset.",
|
|
482
|
+
"Pass the user's image prompt verbatim. Do not embellish, rewrite, add camera/style details, or add negative prompt terms unless the user explicitly asks you to refine the prompt.",
|
|
397
483
|
"Use openai_image with images for local reference images or edit targets; save project assets into the workspace when requested."
|
|
398
484
|
],
|
|
399
485
|
parameters: TOOL_PARAMS,
|
|
400
486
|
async execute(_toolCallId, params: ToolParams, signal, onUpdate, ctx) {
|
|
401
487
|
const cfg = getConfig(ctx);
|
|
402
488
|
const model = resolveModel(params, ctx, cfg);
|
|
489
|
+
const requestParams = { ...params, prompt: resolveToolPrompt(params, ctx) };
|
|
403
490
|
onUpdate?.({ content: [{ type: "text", text: `Requesting OpenAI image via openai-codex/${model}...` }] });
|
|
404
|
-
const result = await generate(
|
|
491
|
+
const result = await generate(requestParams, ctx, signal);
|
|
405
492
|
return {
|
|
406
493
|
content: [
|
|
407
494
|
{ type: "text", text: resultText(result) },
|
|
@@ -419,9 +506,12 @@ export const _imageTest = {
|
|
|
419
506
|
CODEX_RESPONSES_URL,
|
|
420
507
|
DEFAULT_TIMEOUT_MS,
|
|
421
508
|
OPENAI_IMAGE_TOOL,
|
|
509
|
+
OPENAI_IMAGE_COMMAND,
|
|
422
510
|
extractAccountIdFromJwt,
|
|
423
511
|
imageMimeType,
|
|
424
512
|
dataUrlParts,
|
|
425
513
|
extractImageFromEvent,
|
|
514
|
+
displayPath,
|
|
515
|
+
latestUserPromptFromEntries,
|
|
426
516
|
buildRequest
|
|
427
517
|
};
|