pi-better-openai 0.1.19 → 0.1.21

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
@@ -23,7 +23,7 @@ Usage display and image generation require pi's `openai-codex` OAuth credentials
23
23
  1. In pi, run `/login openai-codex`.
24
24
  2. Verify subscription usage with `/openai-usage`, or open `/openai-settings` and check **Diagnostics**.
25
25
  3. The extension reads auth from pi's agent auth store, normally `~/.pi/agent/auth.json`. Do not copy, paste, or commit values from this file.
26
- 4. If `PI_CODING_AGENT_DIR` is set, the auth store and global generated-image directory use that agent directory instead of `~/.pi/agent`.
26
+ 4. If `PI_CODING_AGENT_DIR` is set, the auth store, global extension config, and global generated-image directory use that agent directory instead of `~/.pi/agent`. A leading `~/` is expanded to your home directory.
27
27
 
28
28
  ## Features
29
29
 
@@ -45,7 +45,7 @@ Usage display and image generation require pi's `openai-codex` OAuth credentials
45
45
  The extension reads JSON config from two locations:
46
46
 
47
47
  - Project config: `.pi/extensions/pi-better-openai.json`
48
- - Global config: `~/.pi/agent/extensions/pi-better-openai.json`
48
+ - Global config: `$PI_CODING_AGENT_DIR/extensions/pi-better-openai.json`, defaulting to `~/.pi/agent/extensions/pi-better-openai.json`
49
49
 
50
50
  Project overrides global. Global values fill fields omitted by the project file. Invalid enum values are ignored, and numeric settings are clamped to safe ranges.
51
51
 
@@ -105,7 +105,7 @@ Agents can call the `openai_image` tool directly. Supported parameters:
105
105
 
106
106
  - `prompt` (required): pass the user's image wording verbatim.
107
107
  - `action`: `auto`, `generate`, or `edit`.
108
- - `images`: project-local reference/edit image paths. Paths must stay inside the current workspace and point to readable PNG, JPEG, WebP, or GIF files.
108
+ - `images`: up to five distinct project-local reference/edit image paths. Paths must stay inside the current workspace and point to readable PNG, JPEG, WebP, or GIF files; each file is limited to 20 MB and the combined input to 50 MB.
109
109
  - `model`: Codex image model override, for example `openai-codex/gpt-5.5`.
110
110
  - `outputFormat`: `png`, `jpeg`, or `webp`.
111
111
  - `save`: `project`, `global`, `custom`, or `none`.
@@ -115,7 +115,7 @@ Save modes:
115
115
 
116
116
  - `project` writes to `.pi/generated-images/` in the current project.
117
117
  - `global` writes to the agent `generated-images` directory, normally `~/.pi/agent/generated-images/` or `$PI_CODING_AGENT_DIR/generated-images/`.
118
- - `custom` writes to `saveDir` or `PI_IMAGE_SAVE_DIR`.
118
+ - `custom` writes to `saveDir` or `PI_IMAGE_SAVE_DIR`; relative paths are resolved from the current project.
119
119
  - `none` returns the image without saving it.
120
120
 
121
121
  The repository ignores `.pi/`, so generated images and local config should not be committed.
package/index.ts CHANGED
@@ -9,10 +9,16 @@ import {
9
9
  getSettingsListTheme,
10
10
  type ExtensionAPI,
11
11
  type ExtensionContext,
12
- } from "@mariozechner/pi-coding-agent";
13
- import { Container, Key, matchesKey, SettingsList } from "@mariozechner/pi-tui";
12
+ } from "@earendil-works/pi-coding-agent";
13
+ import { Container, Key, matchesKey, SettingsList } from "@earendil-works/pi-tui";
14
14
  import { CONFIG_BASENAME, STATUS_KEY } from "./src/identity.ts";
15
- import { formatTokens, sanitizeStatusText, truncateToWidth, visibleWidth } from "./src/format.ts";
15
+ import {
16
+ formatTokens,
17
+ redactDiagnosticValue,
18
+ sanitizeStatusText,
19
+ truncateToWidth,
20
+ visibleWidth,
21
+ } from "./src/format.ts";
16
22
  import {
17
23
  DEFAULT_CONFIG,
18
24
  DEFAULT_IMAGE_CONFIG,
@@ -49,6 +55,7 @@ import {
49
55
  type CodexPetPackage,
50
56
  codexHome,
51
57
  describeCodexPetSelectionIssue,
58
+ findCodexPet,
52
59
  findReadyCodexPet,
53
60
  formatNoReadyCodexPetsMessage,
54
61
  listCodexPets,
@@ -91,6 +98,10 @@ function isInlinePetPlacement(placement: PetPlacement): boolean {
91
98
  return placement === "inline-left" || placement === "inline-right" || placement === "badge";
92
99
  }
93
100
 
101
+ function hasTerminalUI(ctx: ExtensionContext): boolean {
102
+ return ctx.mode === "tui" || (ctx.mode === undefined && ctx.hasUI);
103
+ }
104
+
94
105
  function petSizeCellsForPlacement(placement: PetPlacement, sizeCells: number): number {
95
106
  return placement === "badge" ? Math.min(6, sizeCells) : sizeCells;
96
107
  }
@@ -108,19 +119,10 @@ function petSlugFromPickerValue(value: string): string {
108
119
  return value === PET_EMPTY_VALUE ? "" : value;
109
120
  }
110
121
 
111
- function petPickerLookupKey(value: string): string {
112
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
113
- }
114
-
115
122
  function findPickerPet(value: string, pets: CodexPetPackage[]): CodexPetPackage | undefined {
116
- const requested = petPickerLookupKey(value.trim());
117
- if (!requested) return undefined;
118
- return pets.find(
119
- (pet) =>
120
- pet.hasSpritesheet &&
121
- (petPickerLookupKey(pet.slug) === requested ||
122
- petPickerLookupKey(pet.name) === requested ||
123
- (pet.id !== undefined && petPickerLookupKey(pet.id) === requested)),
123
+ return findCodexPet(
124
+ pets.filter((pet) => pet.hasSpritesheet),
125
+ value,
124
126
  );
125
127
  }
126
128
 
@@ -245,12 +247,32 @@ function combineInlinePetFooter(
245
247
  return lines;
246
248
  }
247
249
 
250
+ function textPanel(title: string, lines: string[], done: () => void) {
251
+ return {
252
+ render(width: number) {
253
+ const clipped = lines.map((line) => truncateToWidth(line, width, "..."));
254
+ return [title, "", ...clipped, "", "Esc/q to go back"];
255
+ },
256
+ invalidate() {},
257
+ handleInput(data: string) {
258
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c")) || data === "q") done();
259
+ },
260
+ };
261
+ }
262
+
248
263
  export default function betterOpenAI(pi: ExtensionAPI): void {
249
264
  const fastController = new FastController(SERVICE_TIER);
250
265
  let cachedConfig: ResolvedConfig | undefined;
251
266
  let footerTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
252
267
  let footerInstalled = false;
253
268
  let statusInstalled = false;
269
+ let contextUsageCached = false;
270
+ let cachedContextUsage: ReturnType<ExtensionContext["getContextUsage"]>;
271
+ let cachedContextLeafId: string | null | undefined;
272
+ let cachedContextModel: ExtensionContext["model"];
273
+ let sessionNameCached = false;
274
+ let cachedSessionNameLeafId: string | null | undefined;
275
+ let cachedSessionName: string | undefined;
254
276
  const usageController = new UsageController(config, updateFooter);
255
277
  const petController = new PetFooterController(config, updateFooter, () => footerInstalled);
256
278
 
@@ -310,6 +332,41 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
310
332
  }
311
333
  }
312
334
 
335
+ function invalidateContextUsage(): void {
336
+ contextUsageCached = false;
337
+ cachedContextUsage = undefined;
338
+ cachedContextLeafId = undefined;
339
+ cachedContextModel = undefined;
340
+ }
341
+
342
+ function contextUsage(ctx: ExtensionContext): ReturnType<ExtensionContext["getContextUsage"]> {
343
+ const leafId = ctx.sessionManager.getLeafId();
344
+ const model = ctx.model;
345
+ if (!contextUsageCached || leafId !== cachedContextLeafId || model !== cachedContextModel) {
346
+ cachedContextUsage = ctx.getContextUsage();
347
+ contextUsageCached = true;
348
+ cachedContextLeafId = leafId;
349
+ cachedContextModel = model;
350
+ }
351
+ return cachedContextUsage;
352
+ }
353
+
354
+ function sessionName(ctx: ExtensionContext): string | undefined {
355
+ const leafId = ctx.sessionManager.getLeafId();
356
+ if (!sessionNameCached || leafId !== cachedSessionNameLeafId) {
357
+ cachedSessionName = ctx.sessionManager.getSessionName();
358
+ cachedSessionNameLeafId = leafId;
359
+ sessionNameCached = true;
360
+ }
361
+ return cachedSessionName;
362
+ }
363
+
364
+ function invalidateSessionName(): void {
365
+ sessionNameCached = false;
366
+ cachedSessionNameLeafId = undefined;
367
+ cachedSessionName = undefined;
368
+ }
369
+
313
370
  pi.registerFlag(FLAG, {
314
371
  description: "Start with OpenAI fast mode enabled (service_tier=priority)",
315
372
  type: "boolean",
@@ -353,19 +410,6 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
353
410
  },
354
411
  });
355
412
 
356
- function textPanel(title: string, lines: string[], done: () => void) {
357
- return {
358
- render(width: number) {
359
- const clipped = lines.map((line) => truncateToWidth(line, width, "..."));
360
- return [title, "", ...clipped, "", "Esc/q to go back"];
361
- },
362
- invalidate() {},
363
- handleInput(data: string) {
364
- if (data.includes("\x1b") || data === "escape" || data === "q" || data === "\x03") done();
365
- },
366
- };
367
- }
368
-
369
413
  function buildPetSettingsItems(cfg: ResolvedConfig): SettingsPickerItem[] {
370
414
  return settingsItemsFromDescriptors(PET_SETTING_DESCRIPTORS, cfg, {
371
415
  "pets.slug": {
@@ -608,10 +652,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
608
652
  });
609
653
  }
610
654
 
611
- function buildFastSettingsItems(
612
- ctx: ExtensionContext,
613
- cfg: ResolvedConfig,
614
- ): SettingsPickerItem[] {
655
+ function buildFastSettingsItems(cfg: ResolvedConfig): SettingsPickerItem[] {
615
656
  return [
616
657
  {
617
658
  id: "fast.enabled",
@@ -659,11 +700,13 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
659
700
  id: "config.print",
660
701
  label: "Print config",
661
702
  currentValue: "open",
662
- description: "Show the selected raw config JSON.",
703
+ description: "Show the selected config JSON with sensitive fields redacted.",
663
704
  submenu: (_value, done) =>
664
705
  textPanel(
665
706
  "Config",
666
- JSON.stringify(readRawConfig(cfg.configPath), null, 2).split("\n"),
707
+ JSON.stringify(redactDiagnosticValue(readRawConfig(cfg.configPath)), null, 2).split(
708
+ "\n",
709
+ ),
667
710
  () => done(),
668
711
  ),
669
712
  },
@@ -680,7 +723,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
680
723
  submenu: (_value, done) =>
681
724
  settingsSubmenu(
682
725
  "Fast mode settings",
683
- () => buildFastSettingsItems(ctx, config(ctx)),
726
+ () => buildFastSettingsItems(config(ctx)),
684
727
  ctx,
685
728
  () => done(fastSettingsSummary(ctx, config(ctx))),
686
729
  ),
@@ -803,6 +846,10 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
803
846
  }
804
847
 
805
848
  async function showSettingsPicker(ctx: ExtensionContext): Promise<void> {
849
+ if (!hasTerminalUI(ctx)) {
850
+ ctx.ui.notify("Better OpenAI settings require interactive TUI mode.", "warning");
851
+ return;
852
+ }
806
853
  try {
807
854
  petController.settingsPets = await listCodexPets();
808
855
  } catch {
@@ -887,7 +934,12 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
887
934
  slug: selectedPet.slug,
888
935
  });
889
936
  updateFooter(ctx);
890
- await petController.refresh(ctx, next, true);
937
+ if (hasTerminalUI(ctx)) await petController.refresh(ctx, next, true);
938
+ else
939
+ ctx.ui.notify(
940
+ `Enabled ${selectedPet.name} (${selectedPet.slug}); footer pets render in interactive TUI mode.`,
941
+ "info",
942
+ );
891
943
  },
892
944
  tuck: (ctx) => {
893
945
  writePetConfig(ctx, { enabled: false });
@@ -912,8 +964,14 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
912
964
 
913
965
  const next = writePetConfig(ctx, { slug: selectedPet.slug });
914
966
  updateFooter(ctx);
915
- if (petController.shouldLoadForConfig(next)) await petController.refresh(ctx, next, true);
916
- else {
967
+ if (petController.shouldLoadForConfig(next)) {
968
+ if (hasTerminalUI(ctx)) await petController.refresh(ctx, next, true);
969
+ else
970
+ ctx.ui.notify(
971
+ `Selected ${selectedPet.name} (${selectedPet.slug}); footer pets render in interactive TUI mode.`,
972
+ "info",
973
+ );
974
+ } else {
917
975
  ctx.ui.notify(
918
976
  `Selected ${selectedPet.name} (${selectedPet.slug}) for the footer pet. Use /pets wake to show it.`,
919
977
  "info",
@@ -966,8 +1024,8 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
966
1024
  const branch = footerData.getGitBranch?.();
967
1025
  if (branch) pwd = `${pwd} (${branch})`;
968
1026
 
969
- const sessionName = ctx.sessionManager.getSessionName();
970
- if (sessionName) pwd = `${pwd} • ${sessionName}`;
1027
+ const currentSessionName = sessionName(ctx);
1028
+ if (currentSessionName) pwd = `${pwd} • ${currentSessionName}`;
971
1029
 
972
1030
  const parts: string[] = [];
973
1031
  if (totalInput) parts.push(`↑${formatTokens(totalInput)}`);
@@ -979,11 +1037,11 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
979
1037
  if (totalCost || usingSubscription)
980
1038
  parts.push(`$${totalCost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`);
981
1039
 
982
- const contextUsage = ctx.getContextUsage();
983
- const contextWindow = contextUsage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
984
- const contextPercentValue = contextUsage?.percent ?? 0;
1040
+ const currentContextUsage = contextUsage(ctx);
1041
+ const contextWindow = currentContextUsage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
1042
+ const contextPercentValue = currentContextUsage?.percent ?? 0;
985
1043
  const contextPercent =
986
- contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
1044
+ currentContextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
987
1045
  const contextDisplay =
988
1046
  contextPercent === "?"
989
1047
  ? `?/${formatTokens(contextWindow)} (auto)`
@@ -997,12 +1055,11 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
997
1055
  parts.push(contextText);
998
1056
 
999
1057
  const cfg = config(ctx);
1000
- const cfgForPets = cfg;
1001
- const shouldRenderPet = petController.shouldRenderInFooter(cfgForPets);
1002
- const requestedPetPlacement = cfgForPets.pets.placement;
1058
+ const shouldRenderPet = petController.shouldRenderInFooter(cfg);
1059
+ const requestedPetPlacement = cfg.pets.placement;
1003
1060
  const requestedPetSizeCells = petSizeCellsForPlacement(
1004
1061
  requestedPetPlacement,
1005
- cfgForPets.pets.sizeCells,
1062
+ cfg.pets.sizeCells,
1006
1063
  );
1007
1064
  const inlinePet = Boolean(
1008
1065
  shouldRenderPet &&
@@ -1010,11 +1067,11 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1010
1067
  isInlinePetPlacement(requestedPetPlacement) &&
1011
1068
  width >= requestedPetSizeCells + 32,
1012
1069
  );
1013
- const petRenderSizeCells = inlinePet ? requestedPetSizeCells : cfgForPets.pets.sizeCells;
1070
+ const petRenderSizeCells = inlinePet ? requestedPetSizeCells : cfg.pets.sizeCells;
1014
1071
  const petColumnWidth = Math.min(petRenderSizeCells, Math.max(1, width - 1));
1015
1072
  const footerTextWidth = inlinePet ? Math.max(1, width - petColumnWidth - 2) : width;
1016
1073
 
1017
- const usageStatusLine = usageController.statusLine(ctx, cfg);
1074
+ const usageStatusLine = usageController.statusLine(ctx, cfg, usingSubscription);
1018
1075
  const usageLine = usageStatusLine ? theme.fg("dim", usageStatusLine) : undefined;
1019
1076
 
1020
1077
  let statsLeft = parts.join(" ");
@@ -1027,7 +1084,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1027
1084
  const modelName = ctx.model?.id || "no-model";
1028
1085
  const thinkingLevel = pi.getThinkingLevel();
1029
1086
  const fastSuffix =
1030
- fastController.active && supportsFast(ctx, config(ctx).supportedModels) ? " fast" : "";
1087
+ fastController.active && supportsFast(ctx, cfg.supportedModels) ? " fast" : "";
1031
1088
  let rightWithoutProvider = modelName;
1032
1089
  if (ctx.model?.reasoning) {
1033
1090
  rightWithoutProvider =
@@ -1084,7 +1141,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1084
1141
  textLines.push(truncateToWidth(statusLine, footerTextWidth, theme.fg("dim", "...")));
1085
1142
  }
1086
1143
 
1087
- const petLines = petController.renderPetLines(ctx, cfgForPets, {
1144
+ const petLines = petController.renderPetLines(ctx, cfg, {
1088
1145
  shouldRenderPet,
1089
1146
  freezePetFrame,
1090
1147
  requestedPetPlacement,
@@ -1140,6 +1197,18 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1140
1197
 
1141
1198
  function updateFooter(ctx: ExtensionContext): void {
1142
1199
  const cfg = config(ctx);
1200
+
1201
+ if (!hasTerminalUI(ctx)) {
1202
+ if (cfg.footer.mode === "off") {
1203
+ setStatus(ctx, undefined);
1204
+ return;
1205
+ }
1206
+ const fast = fastController.statusSegment(ctx, cfg);
1207
+ const usage = usageController.statusLine(ctx, cfg);
1208
+ setStatus(ctx, [fast, usage].filter(Boolean).join(" | ") || undefined);
1209
+ return;
1210
+ }
1211
+
1143
1212
  petController.updateActivity(ctx, cfg);
1144
1213
  const shouldRenderPet = petController.shouldRenderInFooter(cfg);
1145
1214
 
@@ -1162,6 +1231,8 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1162
1231
  }
1163
1232
 
1164
1233
  pi.on("session_start", (_event, ctx) => {
1234
+ invalidateContextUsage();
1235
+ invalidateSessionName();
1165
1236
  const nextConfig = refresh(ctx);
1166
1237
  fastController.initializeForSession(ctx, nextConfig, pi.getFlag(FLAG) === true);
1167
1238
  if (
@@ -1172,15 +1243,16 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1172
1243
  if (fastController.desiredActive && !fastController.active) {
1173
1244
  ctx.ui.notify(fastController.unsupportedRequestMessage(ctx, nextConfig), "warning");
1174
1245
  }
1175
- petController.installResizeGuard(ctx);
1246
+ if (hasTerminalUI(ctx)) petController.installResizeGuard(ctx);
1176
1247
  refreshFooterTotals(ctx);
1177
1248
  updateFooter(ctx);
1178
- if (nextConfig.pets.enabled) void petController.refresh(ctx, nextConfig);
1249
+ if (hasTerminalUI(ctx) && nextConfig.pets.enabled) void petController.refresh(ctx, nextConfig);
1179
1250
  usageController.start(ctx);
1180
1251
  if (fastController.active) ctx.ui.notify(fastController.stateText(ctx, nextConfig), "info");
1181
1252
  });
1182
1253
 
1183
1254
  pi.on("agent_start", (_event, ctx) => {
1255
+ invalidateContextUsage();
1184
1256
  petController.agentStart(ctx);
1185
1257
  updateFooter(ctx);
1186
1258
  });
@@ -1200,13 +1272,21 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1200
1272
  updateFooter(ctx);
1201
1273
  });
1202
1274
 
1203
- pi.on("turn_end", (_event, ctx) => {
1204
- refreshFooterTotals(ctx);
1275
+ pi.on("turn_end", (event, ctx) => {
1276
+ invalidateContextUsage();
1277
+ if (event.message?.role === "assistant") {
1278
+ footerTotals.input += event.message.usage.input;
1279
+ footerTotals.output += event.message.usage.output;
1280
+ footerTotals.cacheRead += event.message.usage.cacheRead;
1281
+ footerTotals.cacheWrite += event.message.usage.cacheWrite;
1282
+ footerTotals.cost += event.message.usage.cost.total;
1283
+ } else refreshFooterTotals(ctx);
1205
1284
  updateFooter(ctx);
1206
1285
  void usageController.refresh(ctx);
1207
1286
  });
1208
1287
 
1209
1288
  pi.on("session_compact", (_event, ctx) => {
1289
+ invalidateContextUsage();
1210
1290
  refreshFooterTotals(ctx);
1211
1291
  petController.queueKittyCleanup();
1212
1292
  petController.resetRenderCache();
@@ -1214,6 +1294,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1214
1294
  });
1215
1295
 
1216
1296
  pi.on("session_tree", (_event, ctx) => {
1297
+ invalidateContextUsage();
1217
1298
  refreshFooterTotals(ctx);
1218
1299
  petController.queueKittyCleanup();
1219
1300
  petController.resetRenderCache();
@@ -1221,6 +1302,7 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1221
1302
  });
1222
1303
 
1223
1304
  pi.on("model_select", (event, ctx) => {
1305
+ invalidateContextUsage();
1224
1306
  const cfg = config(ctx);
1225
1307
  const wasActive = fastController.active;
1226
1308
  fastController.applyDesiredState(ctx, cfg);
@@ -1238,6 +1320,8 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1238
1320
  });
1239
1321
 
1240
1322
  pi.on("session_shutdown", () => {
1323
+ invalidateContextUsage();
1324
+ invalidateSessionName();
1241
1325
  usageController.shutdown();
1242
1326
  petController.shutdown();
1243
1327
  });
@@ -1245,6 +1329,10 @@ export default function betterOpenAI(pi: ExtensionAPI): void {
1245
1329
  pi.on("before_provider_request", (event, ctx) => {
1246
1330
  return fastController.injectProviderPayload(event, ctx, config(ctx));
1247
1331
  });
1332
+
1333
+ pi.on("message_start", invalidateContextUsage);
1334
+ pi.on("message_update", invalidateContextUsage);
1335
+ pi.on("message_end", invalidateContextUsage);
1248
1336
  }
1249
1337
 
1250
1338
  export const _test = {
@@ -1273,6 +1361,7 @@ export const _test = {
1273
1361
  formatPercent,
1274
1362
  formatUsageSnapshot,
1275
1363
  readCodexAuth,
1364
+ textPanel,
1276
1365
  imageTest: _imageTest,
1277
1366
  petsTest: _petsTest,
1278
1367
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-openai",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "Personal pi extension that improves OpenAI with fast mode, usage stats, and footer polish.",
5
5
  "keywords": [
6
6
  "fast",
@@ -41,6 +41,8 @@
41
41
  "sharp": "^0.34.5"
42
42
  },
43
43
  "devDependencies": {
44
+ "@earendil-works/pi-coding-agent": "0.80.6",
45
+ "@earendil-works/pi-tui": "0.80.6",
44
46
  "@types/node": "^24.13.2",
45
47
  "oxfmt": "^0.47.0",
46
48
  "oxlint": "^1.62.0",
@@ -48,11 +50,11 @@
48
50
  "vitest": "^4.1.5"
49
51
  },
50
52
  "peerDependencies": {
51
- "@mariozechner/pi-coding-agent": ">=0.57.0",
52
- "@mariozechner/pi-tui": "*"
53
+ "@earendil-works/pi-coding-agent": "*",
54
+ "@earendil-works/pi-tui": "*"
53
55
  },
54
56
  "engines": {
55
- "node": ">=20.6.0"
57
+ "node": ">=22.19.0"
56
58
  },
57
59
  "pi": {
58
60
  "extensions": [
package/src/codex-auth.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
2
  import { join } from "node:path";
4
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
3
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import { piAgentDir } from "./paths.ts";
5
5
 
6
- const AGENT_DIR = process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent");
7
- export const AUTH_FILE = join(AGENT_DIR, "auth.json");
6
+ export const AUTH_FILE = join(piAgentDir(), "auth.json");
8
7
 
9
8
  export type CodexCredentials = {
10
9
  accessToken: string;
@@ -15,6 +14,30 @@ export type CodexCredentialsWithSource = CodexCredentials & {
15
14
  source: "modelRegistry" | "authFile";
16
15
  };
17
16
 
17
+ function waitForSignal<T>(operation: Promise<T>, signal?: AbortSignal): Promise<T> {
18
+ if (!signal) return operation;
19
+ if (signal.aborted) return Promise.reject(signal.reason ?? new Error("Operation was aborted."));
20
+
21
+ return new Promise<T>((resolve, reject) => {
22
+ const onAbort = () => {
23
+ cleanup();
24
+ reject(signal.reason ?? new Error("Operation was aborted."));
25
+ };
26
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
27
+ signal.addEventListener("abort", onAbort, { once: true });
28
+ void operation.then(
29
+ (value) => {
30
+ cleanup();
31
+ resolve(value);
32
+ },
33
+ (error: unknown) => {
34
+ cleanup();
35
+ reject(error);
36
+ },
37
+ );
38
+ });
39
+ }
40
+
18
41
  function decodeBase64Url(value: string): string {
19
42
  const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
20
43
  const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
@@ -79,11 +102,13 @@ export function readCodexAuth(): CodexCredentials | undefined {
79
102
  access?: string | null;
80
103
  accountId?: string | null;
81
104
  account_id?: string | null;
105
+ expires?: number | null;
82
106
  }
83
107
  | undefined
84
108
  >;
85
109
  const entry = auth["openai-codex"];
86
110
  if (entry?.type !== "oauth") return undefined;
111
+ if (typeof entry.expires === "number" && Date.now() >= entry.expires) return undefined;
87
112
  const accessToken = entry.access?.trim();
88
113
  const accountId = (entry.accountId ?? entry.account_id)?.trim();
89
114
  return accessToken && accountId ? { accessToken, accountId } : undefined;
@@ -94,10 +119,16 @@ export function readCodexAuth(): CodexCredentials | undefined {
94
119
 
95
120
  export async function getCodexCredentials(
96
121
  ctx?: Pick<ExtensionContext, "modelRegistry">,
122
+ signal?: AbortSignal,
97
123
  ): Promise<CodexCredentialsWithSource | undefined> {
98
- const registryToken = await ctx?.modelRegistry
99
- ?.getApiKeyForProvider("openai-codex")
100
- .catch(() => undefined);
124
+ if (signal?.aborted) throw signal.reason ?? new Error("Operation was aborted.");
125
+ const registryRequest = ctx?.modelRegistry?.getApiKeyForProvider("openai-codex");
126
+ const registryToken = registryRequest
127
+ ? await waitForSignal(
128
+ registryRequest.catch(() => undefined),
129
+ signal,
130
+ )
131
+ : undefined;
101
132
  const registryCredentials = parseCodexRegistryCredentials(registryToken);
102
133
  if (registryCredentials) return { ...registryCredentials, source: "modelRegistry" };
103
134
  const auth = readCodexAuth();
package/src/config.ts CHANGED
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
  import { CONFIG_BASENAME, logPrefix } from "./identity.ts";
5
+ import { piAgentDir } from "./paths.ts";
5
6
 
6
7
  export const FOOTER_MODES = ["replace", "status", "off"] as const;
7
8
  export const IMAGE_SAVE_MODES = ["none", "project", "global", "custom"] as const;
@@ -409,10 +410,10 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
409
410
  return typeof value === "object" && value !== null && !Array.isArray(value);
410
411
  }
411
412
 
412
- export function configPaths(cwd: string, home = homedir()) {
413
+ export function configPaths(cwd: string, home = homedir(), env = process.env) {
413
414
  return {
414
415
  project: join(cwd, ".pi", "extensions", CONFIG_BASENAME),
415
- global: join(home, ".pi", "agent", "extensions", CONFIG_BASENAME),
416
+ global: join(piAgentDir(env, home), "extensions", CONFIG_BASENAME),
416
417
  };
417
418
  }
418
419
 
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { ResolvedConfig, SupportedModel } from "./config.ts";
3
3
  import { isRecord } from "./config.ts";
4
4