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 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 a local checkout:
7
+ Install from GitHub:
12
8
 
13
9
  ```bash
14
- pi install /path/to/pi-better-openai
10
+ pi install github:mattleong/pi-better-openai
15
11
  ```
16
12
 
17
- For a project-local install instead of a global install, add `-l`:
13
+ Or install from npm:
18
14
 
19
15
  ```bash
20
- pi install -l /path/to/pi-better-openai
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
- if (!ctx.model) return false;
61
- return supportedModels.some((model) => model.provider === ctx.model.provider && model.id === ctx.model.id);
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.4",
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
- "extensions/",
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
- "./extensions/pi-better-openai.ts"
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 extractImageFromEvent(event: unknown, fallbackMimeType: string): Omit<CodexImageResult, "savedPath" | "model" | "action" | "outputFormat"> | undefined {
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<Omit<CodexImageResult, "savedPath" | "model" | "action" | "outputFormat">> {
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: Omit<CodexImageResult, "savedPath" | "model" | "action" | "outputFormat"> | undefined;
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
- onUpdate?.({ content: [{ type: "text", text: `Requesting OpenAI image via openai-codex/${model}...` }] });
427
- const result = await generate(params, ctx, signal);
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