pi-better-openai 0.1.4 → 0.1.7
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 +11 -26
- package/{extensions/pi-better-openai.ts → index.ts} +8 -7
- package/package.json +10 -4
- package/{extensions → src}/image.ts +82 -8
- /package/{extensions → src}/config.ts +0 -0
- /package/{extensions → src}/format.ts +0 -0
- /package/{extensions → src}/identity.ts +0 -0
- /package/{extensions → src}/usage.ts +0 -0
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
|
|
@@ -50,3 +28,10 @@ pi --fast
|
|
|
50
28
|
- `/openai-image <prompt>` generates an image directly.
|
|
51
29
|
- `/openai-usage` shows current OpenAI subscription usage.
|
|
52
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" />
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* enabled and the selected model is in the configured allow-list.
|
|
6
6
|
*/
|
|
7
7
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
8
|
-
import { CONFIG_BASENAME, STATUS_KEY } from "./identity.ts";
|
|
9
|
-
import { formatTokens, sanitizeStatusText, truncateToWidth, visibleWidth } from "./format.ts";
|
|
8
|
+
import { CONFIG_BASENAME, STATUS_KEY } from "./src/identity.ts";
|
|
9
|
+
import { formatTokens, sanitizeStatusText, truncateToWidth, visibleWidth } from "./src/format.ts";
|
|
10
10
|
import {
|
|
11
11
|
DEFAULT_CONFIG,
|
|
12
12
|
DEFAULT_IMAGE_CONFIG,
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
readRawConfig,
|
|
26
26
|
resolveConfig,
|
|
27
27
|
writeConfig
|
|
28
|
-
} from "./config.ts";
|
|
28
|
+
} from "./src/config.ts";
|
|
29
29
|
import {
|
|
30
30
|
AUTH_FILE,
|
|
31
31
|
type UsageSnapshot,
|
|
@@ -35,8 +35,8 @@ import {
|
|
|
35
35
|
parseUsageSnapshot,
|
|
36
36
|
readCodexAuth,
|
|
37
37
|
requestCodexUsage
|
|
38
|
-
} from "./usage.ts";
|
|
39
|
-
import { registerOpenAIImage, _imageTest } from "./image.ts";
|
|
38
|
+
} from "./src/usage.ts";
|
|
39
|
+
import { registerOpenAIImage, _imageTest } from "./src/image.ts";
|
|
40
40
|
|
|
41
41
|
const COMMAND = "fast";
|
|
42
42
|
const OPENAI_STATUS_COMMAND = "openai-usage";
|
|
@@ -57,8 +57,9 @@ function currentModelKey(ctx: ExtensionContext): string {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function supportsFast(ctx: ExtensionContext, supportedModels: SupportedModel[]): boolean {
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
const current = ctx.model;
|
|
61
|
+
if (!current) return false;
|
|
62
|
+
return supportedModels.some((model) => model.provider === current.provider && model.id === current.id);
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
function modelList(supportedModels: SupportedModel[]): string {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-openai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Personal pi extension that improves OpenAI with fast mode, usage stats, and footer polish.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fast",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"url": "git+https://github.com/mattleong/pi-better-openai.git"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
|
-
"
|
|
22
|
+
"index.ts",
|
|
23
|
+
"src/",
|
|
23
24
|
"README.md",
|
|
24
25
|
"LICENSE"
|
|
25
26
|
],
|
|
@@ -28,7 +29,8 @@
|
|
|
28
29
|
"access": "public"
|
|
29
30
|
},
|
|
30
31
|
"scripts": {
|
|
31
|
-
"test": "node tests/basic.mjs"
|
|
32
|
+
"test": "node tests/basic.mjs",
|
|
33
|
+
"typecheck": "tsc --noEmit"
|
|
32
34
|
},
|
|
33
35
|
"peerDependencies": {
|
|
34
36
|
"@mariozechner/pi-coding-agent": ">=0.57.0",
|
|
@@ -36,7 +38,11 @@
|
|
|
36
38
|
},
|
|
37
39
|
"pi": {
|
|
38
40
|
"extensions": [
|
|
39
|
-
"./
|
|
41
|
+
"./index.ts"
|
|
40
42
|
]
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^25.6.0",
|
|
46
|
+
"typescript": "^6.0.3"
|
|
41
47
|
}
|
|
42
48
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
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";
|
|
@@ -23,7 +23,7 @@ export type ImageOutputFormat = typeof IMAGE_OUTPUT_FORMATS[number];
|
|
|
23
23
|
const TOOL_PARAMS = {
|
|
24
24
|
type: "object",
|
|
25
25
|
properties: {
|
|
26
|
-
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." },
|
|
27
27
|
action: { type: "string", enum: IMAGE_ACTIONS, description: "Whether to generate a new image, edit/reference provided images, or let the model decide." },
|
|
28
28
|
images: {
|
|
29
29
|
type: "array",
|
|
@@ -74,6 +74,8 @@ export type CodexImageResult = {
|
|
|
74
74
|
outputFormat: ImageOutputFormat;
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
+
type ExtractedImageResult = Omit<CodexImageResult, "prompt" | "savedPath" | "model" | "action" | "outputFormat">;
|
|
78
|
+
|
|
77
79
|
export type ImageGenerationDebug = {
|
|
78
80
|
authFound: boolean;
|
|
79
81
|
authSource?: string;
|
|
@@ -224,7 +226,11 @@ function asImageResultItem(value: unknown): { id?: string; status?: string; revi
|
|
|
224
226
|
return value as { id?: string; status?: string; revised_prompt?: string; result?: string; b64_json?: string };
|
|
225
227
|
}
|
|
226
228
|
|
|
227
|
-
function
|
|
229
|
+
function isImageContent(value: unknown): value is { type: "image"; data: string; mimeType: string } {
|
|
230
|
+
return isRecord(value) && value.type === "image" && typeof value.data === "string" && typeof value.mimeType === "string";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function extractImageFromEvent(event: unknown, fallbackMimeType: string): ExtractedImageResult | undefined {
|
|
228
234
|
if (!isRecord(event)) return undefined;
|
|
229
235
|
const item = asImageResultItem(event.item) ?? asImageResultItem(event);
|
|
230
236
|
if (item) {
|
|
@@ -247,12 +253,12 @@ function extractImageFromEvent(event: unknown, fallbackMimeType: string): Omit<C
|
|
|
247
253
|
return undefined;
|
|
248
254
|
}
|
|
249
255
|
|
|
250
|
-
async function parseSseForImage(response: Response, fallbackMimeType: string, signal?: AbortSignal): Promise<
|
|
256
|
+
async function parseSseForImage(response: Response, fallbackMimeType: string, signal?: AbortSignal): Promise<ExtractedImageResult> {
|
|
251
257
|
if (!response.body) throw new Error("No response body from Codex image request.");
|
|
252
258
|
const reader = response.body.getReader();
|
|
253
259
|
const decoder = new TextDecoder();
|
|
254
260
|
let buffer = "";
|
|
255
|
-
let lastImage:
|
|
261
|
+
let lastImage: ExtractedImageResult | undefined;
|
|
256
262
|
try {
|
|
257
263
|
while (true) {
|
|
258
264
|
if (signal?.aborted) throw new Error("Image request was aborted.");
|
|
@@ -338,6 +344,39 @@ async function requestCodexImage(params: ToolParams, ctx: ExtensionContext, cfg:
|
|
|
338
344
|
return { ...parsed, prompt: params.prompt, savedPath, model, action, outputFormat };
|
|
339
345
|
}
|
|
340
346
|
|
|
347
|
+
function displayPath(path: string): string {
|
|
348
|
+
const home = homedir();
|
|
349
|
+
if (!home) return path;
|
|
350
|
+
if (path === home) return "~";
|
|
351
|
+
const homePrefix = home.endsWith(sep) ? home : `${home}${sep}`;
|
|
352
|
+
return path.startsWith(homePrefix) ? `~/${path.slice(homePrefix.length)}` : path;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function textFromMessageContent(content: unknown): string | undefined {
|
|
356
|
+
if (typeof content === "string") return content.trim() || undefined;
|
|
357
|
+
if (!Array.isArray(content)) return undefined;
|
|
358
|
+
const text = content
|
|
359
|
+
.filter((part) => isRecord(part) && part.type === "text" && typeof part.text === "string")
|
|
360
|
+
.map((part) => (part as { text: string }).text)
|
|
361
|
+
.join("\n")
|
|
362
|
+
.trim();
|
|
363
|
+
return text || undefined;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function latestUserPromptFromEntries(entries: unknown[]): string | undefined {
|
|
367
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
368
|
+
const entry = entries[i];
|
|
369
|
+
if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message) || entry.message.role !== "user") continue;
|
|
370
|
+
const text = textFromMessageContent(entry.message.content);
|
|
371
|
+
if (text) return text;
|
|
372
|
+
}
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function resolveToolPrompt(params: ToolParams, ctx: ExtensionContext): string {
|
|
377
|
+
return latestUserPromptFromEntries(ctx.sessionManager.getEntries()) ?? params.prompt;
|
|
378
|
+
}
|
|
379
|
+
|
|
341
380
|
function resultText(result: CodexImageResult): string {
|
|
342
381
|
const parts = [
|
|
343
382
|
`Generated image via openai-codex/${result.model}.`,
|
|
@@ -345,7 +384,7 @@ function resultText(result: CodexImageResult): string {
|
|
|
345
384
|
`Prompt: ${result.prompt}`
|
|
346
385
|
];
|
|
347
386
|
if (result.revisedPrompt) parts.push(`Revised prompt: ${result.revisedPrompt}`);
|
|
348
|
-
if (result.savedPath) parts.push(`Saved: ${result.savedPath}`);
|
|
387
|
+
if (result.savedPath) parts.push(`Saved: ${displayPath(result.savedPath)}`);
|
|
349
388
|
return parts.join("\n");
|
|
350
389
|
}
|
|
351
390
|
|
|
@@ -388,6 +427,37 @@ export function registerOpenAIImage(pi: ExtensionAPI, getConfig: (ctx: Extension
|
|
|
388
427
|
};
|
|
389
428
|
}
|
|
390
429
|
|
|
430
|
+
void import("@mariozechner/pi-tui").then(({ Box, Container, Image, Text }) => {
|
|
431
|
+
pi.registerMessageRenderer<CodexImageResult>("openai-image", (message, _options, theme) => {
|
|
432
|
+
const result = message.details;
|
|
433
|
+
const text = result && isRecord(result)
|
|
434
|
+
? resultText(result as CodexImageResult)
|
|
435
|
+
: typeof message.content === "string"
|
|
436
|
+
? message.content
|
|
437
|
+
: message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
|
|
438
|
+
let image: { data: string; mimeType: string; savedPath?: string } | undefined;
|
|
439
|
+
if (result && isRecord(result) && typeof result.data === "string" && typeof result.mimeType === "string") {
|
|
440
|
+
image = { data: result.data, mimeType: result.mimeType, savedPath: typeof result.savedPath === "string" ? result.savedPath : undefined };
|
|
441
|
+
} else if (Array.isArray(message.content)) {
|
|
442
|
+
const imagePart = message.content.find(isImageContent);
|
|
443
|
+
if (imagePart) image = { data: imagePart.data, mimeType: imagePart.mimeType };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const container = new Container();
|
|
447
|
+
const box = new Box(1, 1, (line) => theme.bg("customMessageBg", line));
|
|
448
|
+
box.addChild(new Text(`${theme.fg("accent", theme.bold("[openai-image]"))}\n\n${text}`, 0, 0));
|
|
449
|
+
if (image) {
|
|
450
|
+
box.addChild(new Image(image.data, image.mimeType, { fallbackColor: (line) => theme.fg("dim", line) }, {
|
|
451
|
+
maxWidthCells: 80,
|
|
452
|
+
maxHeightCells: 24,
|
|
453
|
+
filename: "savedPath" in image && typeof image.savedPath === "string" ? image.savedPath : undefined
|
|
454
|
+
}));
|
|
455
|
+
}
|
|
456
|
+
container.addChild(box);
|
|
457
|
+
return container;
|
|
458
|
+
});
|
|
459
|
+
}).catch(() => undefined);
|
|
460
|
+
|
|
391
461
|
pi.registerCommand(OPENAI_IMAGE_COMMAND, {
|
|
392
462
|
description: "Generate an image with OpenAI Codex image generation",
|
|
393
463
|
handler: async (args, ctx) => {
|
|
@@ -417,14 +487,16 @@ export function registerOpenAIImage(pi: ExtensionAPI, getConfig: (ctx: Extension
|
|
|
417
487
|
promptSnippet: "Generate or edit raster images via OpenAI Codex subscription auth.",
|
|
418
488
|
promptGuidelines: [
|
|
419
489
|
"Use openai_image when the user asks to generate or edit a raster image, photo, illustration, mockup, texture, sprite, or bitmap asset.",
|
|
490
|
+
"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.",
|
|
420
491
|
"Use openai_image with images for local reference images or edit targets; save project assets into the workspace when requested."
|
|
421
492
|
],
|
|
422
493
|
parameters: TOOL_PARAMS,
|
|
423
494
|
async execute(_toolCallId, params: ToolParams, signal, onUpdate, ctx) {
|
|
424
495
|
const cfg = getConfig(ctx);
|
|
425
496
|
const model = resolveModel(params, ctx, cfg);
|
|
426
|
-
|
|
427
|
-
|
|
497
|
+
const requestParams = { ...params, prompt: resolveToolPrompt(params, ctx) };
|
|
498
|
+
onUpdate?.({ content: [{ type: "text", text: `Requesting OpenAI image via openai-codex/${model}...` }], details: undefined });
|
|
499
|
+
const result = await generate(requestParams, ctx, signal);
|
|
428
500
|
return {
|
|
429
501
|
content: [
|
|
430
502
|
{ type: "text", text: resultText(result) },
|
|
@@ -447,5 +519,7 @@ export const _imageTest = {
|
|
|
447
519
|
imageMimeType,
|
|
448
520
|
dataUrlParts,
|
|
449
521
|
extractImageFromEvent,
|
|
522
|
+
displayPath,
|
|
523
|
+
latestUserPromptFromEntries,
|
|
450
524
|
buildRequest
|
|
451
525
|
};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|