pi-better-openai 0.1.16 → 0.1.17

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/pets.ts +110 -91
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-openai",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Personal pi extension that improves OpenAI with fast mode, usage stats, and footer polish.",
5
5
  "keywords": [
6
6
  "fast",
package/src/pets.ts CHANGED
@@ -21,6 +21,7 @@ const PET_COLUMNS = 8;
21
21
  const PET_ROWS = 9;
22
22
  const DEFAULT_CELL_WIDTH = 192;
23
23
  const DEFAULT_CELL_HEIGHT = 208;
24
+ const DEFAULT_SPRITESHEET_PATH = "spritesheet.webp";
24
25
  const EXPECTED_ATLAS_WIDTH = DEFAULT_CELL_WIDTH * PET_COLUMNS;
25
26
  const EXPECTED_ATLAS_HEIGHT = DEFAULT_CELL_HEIGHT * PET_ROWS;
26
27
 
@@ -96,6 +97,14 @@ function sanitizePetAssetPathText(value: string): string | undefined {
96
97
  return sanitized || undefined;
97
98
  }
98
99
 
100
+ function sanitizePetDisplayField(value: unknown): string | undefined {
101
+ return typeof value === "string" ? sanitizePetDisplayText(value) : undefined;
102
+ }
103
+
104
+ function sanitizePetAssetPathField(value: unknown): string | undefined {
105
+ return typeof value === "string" ? sanitizePetAssetPathText(value) : undefined;
106
+ }
107
+
99
108
  export function codexHome(env = process.env, home = homedir()): string {
100
109
  return env.CODEX_HOME?.trim() || join(home, ".codex");
101
110
  }
@@ -109,21 +118,17 @@ function petInfoFromJson(
109
118
  fallback: string,
110
119
  ): { id?: string; name: string; description?: string; spritesheetPath: string } {
111
120
  const fallbackName = sanitizePetDisplayText(fallback) ?? "Unnamed pet";
112
- if (!isRecord(value)) return { name: fallbackName, spritesheetPath: "spritesheet.webp" };
113
- const id = typeof value.id === "string" ? sanitizePetDisplayText(value.id) : undefined;
114
- const displayName =
115
- typeof value.displayName === "string" ? sanitizePetDisplayText(value.displayName) : undefined;
121
+ if (!isRecord(value)) return { name: fallbackName, spritesheetPath: DEFAULT_SPRITESHEET_PATH };
122
+
123
+ const id = sanitizePetDisplayField(value.id);
116
124
  const name =
117
- displayName ??
118
- (typeof value.name === "string" ? sanitizePetDisplayText(value.name) : undefined) ??
125
+ sanitizePetDisplayField(value.displayName) ??
126
+ sanitizePetDisplayField(value.name) ??
119
127
  id ??
120
128
  fallbackName;
121
- const description =
122
- typeof value.description === "string" ? sanitizePetDisplayText(value.description) : undefined;
129
+ const description = sanitizePetDisplayField(value.description);
123
130
  const spritesheetPath =
124
- typeof value.spritesheetPath === "string"
125
- ? (sanitizePetAssetPathText(value.spritesheetPath) ?? "spritesheet.webp")
126
- : "spritesheet.webp";
131
+ sanitizePetAssetPathField(value.spritesheetPath) ?? DEFAULT_SPRITESHEET_PATH;
127
132
  return { id, name, description, spritesheetPath };
128
133
  }
129
134
 
@@ -224,8 +229,10 @@ function petMatchesLookup(pet: CodexPetPackage, requested: string): boolean {
224
229
  }
225
230
 
226
231
  export function findCodexPet(pets: CodexPetPackage[], value?: string): CodexPetPackage | undefined {
227
- const requested = value?.trim() ? petLookupKey(value.trim()) : "";
228
- if (!requested) return undefined;
232
+ const requestedText = value?.trim();
233
+ if (!requestedText) return undefined;
234
+
235
+ const requested = petLookupKey(requestedText);
229
236
  return pets.find((pet) => petMatchesLookup(pet, requested));
230
237
  }
231
238
 
@@ -233,10 +240,11 @@ export function findReadyCodexPet(
233
240
  pets: CodexPetPackage[],
234
241
  value?: string,
235
242
  ): CodexPetPackage | undefined {
236
- const ready = pets.filter((pet) => pet.hasSpritesheet);
237
- if (!value?.trim()) return ready[0];
238
- const requested = petLookupKey(value.trim());
239
- return ready.find((pet) => petMatchesLookup(pet, requested));
243
+ const requestedText = value?.trim();
244
+ if (!requestedText) return pets.find((pet) => pet.hasSpritesheet);
245
+
246
+ const requested = petLookupKey(requestedText);
247
+ return pets.find((pet) => pet.hasSpritesheet && petMatchesLookup(pet, requested));
240
248
  }
241
249
 
242
250
  function selectPet(pets: CodexPetPackage[], slug?: string): CodexPetPackage | undefined {
@@ -255,7 +263,7 @@ export function formatCodexPetSetupInstructions(home = codexHome()): string {
255
263
  "",
256
264
  "Expected files:",
257
265
  " pet.json",
258
- " spritesheet.webp",
266
+ ` ${DEFAULT_SPRITESHEET_PATH}`,
259
267
  "",
260
268
  "Create one:",
261
269
  " $skill-installer hatch-pet",
@@ -323,7 +331,7 @@ export function formatNoReadyCodexPetsMessage(pets: CodexPetPackage[], home = co
323
331
  return [
324
332
  "Found custom Codex pets, but none are ready.",
325
333
  "",
326
- `A ready pet needs pet.json and a readable ${EXPECTED_ATLAS_WIDTH}x${EXPECTED_ATLAS_HEIGHT} spritesheet.webp atlas in its pet folder.`,
334
+ `A ready pet needs pet.json and a readable ${EXPECTED_ATLAS_WIDTH}x${EXPECTED_ATLAS_HEIGHT} ${DEFAULT_SPRITESHEET_PATH} atlas in its pet folder.`,
327
335
  "Run /pets list to see what needs fixing, or create a new pet:",
328
336
  "",
329
337
  formatCodexPetSetupInstructions(home),
@@ -354,8 +362,6 @@ export async function loadCodexPet(
354
362
  `Invalid Codex pet atlas dimensions: ${metadata.width ?? "?"}x${metadata.height ?? "?"}; expected ${EXPECTED_ATLAS_WIDTH}x${EXPECTED_ATLAS_HEIGHT}.`,
355
363
  );
356
364
  }
357
- const cellWidth = DEFAULT_CELL_WIDTH;
358
- const cellHeight = DEFAULT_CELL_HEIGHT;
359
365
  const states = {} as Record<PetState, PetFrame[]>;
360
366
  const imageProtocol = getCapabilities().images;
361
367
  const useKitty = imageProtocol === "kitty";
@@ -368,10 +374,13 @@ export async function loadCodexPet(
368
374
  const targetHeightPx = targetWidthPx
369
375
  ? Math.max(
370
376
  1,
371
- Math.ceil((cellHeight * targetWidthPx) / cellWidth / cellDimensions.heightPx) *
372
- cellDimensions.heightPx,
377
+ Math.ceil(
378
+ (DEFAULT_CELL_HEIGHT * targetWidthPx) / DEFAULT_CELL_WIDTH / cellDimensions.heightPx,
379
+ ) * cellDimensions.heightPx,
373
380
  )
374
381
  : undefined;
382
+ const outputWidthPx = targetWidthPx ?? DEFAULT_CELL_WIDTH;
383
+ const outputHeightPx = targetHeightPx ?? DEFAULT_CELL_HEIGHT;
375
384
 
376
385
  for (const [state, animation] of Object.entries(PET_ANIMATION_ROWS) as Array<
377
386
  [PetState, (typeof PET_ANIMATION_ROWS)[PetState]]
@@ -379,23 +388,21 @@ export async function loadCodexPet(
379
388
  states[state] = [];
380
389
  for (let column = 0; column < animation.durations.length; column++) {
381
390
  const durationMs = animation.durations[column] ?? 150;
382
- const frameWidthPx = targetWidthPx ?? cellWidth;
383
- const frameHeightPx = targetHeightPx ?? cellHeight;
384
391
  if (!imageProtocol) {
385
392
  states[state].push({
386
393
  mimeType: "image/png",
387
394
  durationMs,
388
- widthPx: frameWidthPx,
389
- heightPx: frameHeightPx,
395
+ widthPx: outputWidthPx,
396
+ heightPx: outputHeightPx,
390
397
  });
391
398
  continue;
392
399
  }
393
400
 
394
401
  let frame = source.clone().extract({
395
- left: column * cellWidth,
396
- top: animation.row * cellHeight,
397
- width: cellWidth,
398
- height: cellHeight,
402
+ left: column * DEFAULT_CELL_WIDTH,
403
+ top: animation.row * DEFAULT_CELL_HEIGHT,
404
+ width: DEFAULT_CELL_WIDTH,
405
+ height: DEFAULT_CELL_HEIGHT,
399
406
  });
400
407
  if (targetWidthPx && targetHeightPx) {
401
408
  frame = frame.resize(targetWidthPx, targetHeightPx, {
@@ -411,8 +418,8 @@ export async function loadCodexPet(
411
418
  kittyImageId: useKitty ? kittyImageBase + kittyFrameOffset++ : undefined,
412
419
  mimeType: "image/png",
413
420
  durationMs,
414
- widthPx: frameWidthPx,
415
- heightPx: frameHeightPx,
421
+ widthPx: outputWidthPx,
422
+ heightPx: outputHeightPx,
416
423
  });
417
424
  }
418
425
  }
@@ -452,24 +459,30 @@ export function nextAnimationFrameDelayMs(
452
459
  if (cursor < duration) return Math.max(1, Math.ceil(duration - cursor));
453
460
  cursor -= duration;
454
461
  }
455
- return Math.max(1, Math.ceil(frames[0]?.durationMs ?? 120));
462
+ return Math.max(1, Math.ceil(frames[0].durationMs));
456
463
  }
457
464
 
458
- function markCodexPetKittyFramesUnuploaded(pet?: LoadedCodexPet): void {
465
+ function forEachCodexPetFrame(
466
+ pet: LoadedCodexPet | undefined,
467
+ visit: (frame: PetFrame) => void,
468
+ ): void {
459
469
  if (!pet) return;
460
470
  for (const frames of Object.values(pet.states)) {
461
- for (const frame of frames) frame.kittyUploaded = false;
471
+ for (const frame of frames) visit(frame);
462
472
  }
463
473
  }
464
474
 
475
+ function markCodexPetKittyFramesUnuploaded(pet?: LoadedCodexPet): void {
476
+ forEachCodexPetFrame(pet, (frame) => {
477
+ frame.kittyUploaded = false;
478
+ });
479
+ }
480
+
465
481
  function codexPetKittyImageIds(pet?: LoadedCodexPet): number[] {
466
- if (!pet) return [];
467
482
  const imageIds = new Set<number>();
468
- for (const frames of Object.values(pet.states)) {
469
- for (const frame of frames) {
470
- if (frame.kittyImageId !== undefined) imageIds.add(frame.kittyImageId);
471
- }
472
- }
483
+ forEachCodexPetFrame(pet, (frame) => {
484
+ if (frame.kittyImageId !== undefined) imageIds.add(frame.kittyImageId);
485
+ });
473
486
  return Array.from(imageIds);
474
487
  }
475
488
 
@@ -496,18 +509,18 @@ function encodeKittyRawRgba(frame: PetFrame, imageId: number): string {
496
509
  `v=${frame.heightPx}`,
497
510
  `i=${imageId}`,
498
511
  "q=2",
499
- ];
512
+ ].join(",");
500
513
  if (rawRgbaData.length <= chunkSize) {
501
- return `\x1b_G${params.join(",")};${rawRgbaData}\x1b\\`;
514
+ return `\x1b_G${params};${rawRgbaData}\x1b\\`;
502
515
  }
503
516
 
504
517
  const chunks: string[] = [];
505
518
  for (let offset = 0; offset < rawRgbaData.length; offset += chunkSize) {
506
519
  const chunk = rawRgbaData.slice(offset, offset + chunkSize);
507
- const first = offset === 0;
508
- const last = offset + chunkSize >= rawRgbaData.length;
509
- if (first) chunks.push(`\x1b_G${params.join(",")},m=1;${chunk}\x1b\\`);
510
- else chunks.push(`\x1b_Gm=${last ? 0 : 1};${chunk}\x1b\\`);
520
+ const isFirstChunk = offset === 0;
521
+ const isLastChunk = offset + chunkSize >= rawRgbaData.length;
522
+ if (isFirstChunk) chunks.push(`\x1b_G${params},m=1;${chunk}\x1b\\`);
523
+ else chunks.push(`\x1b_Gm=${isLastChunk ? 0 : 1};${chunk}\x1b\\`);
511
524
  }
512
525
  return chunks.join("");
513
526
  }
@@ -549,7 +562,7 @@ export class CodexPetKittyManager {
549
562
  }
550
563
 
551
564
  renderFrame(frame: PetFrame, width: number, options: { sizeCells: number }): string[] {
552
- const columns = Math.max(1, Math.min(Math.max(1, width - 2), options.sizeCells));
565
+ const columns = Math.max(1, Math.min(width - 2, options.sizeCells));
553
566
  const rows = calculateImageRows(
554
567
  { widthPx: frame.widthPx, heightPx: frame.heightPx },
555
568
  columns,
@@ -624,13 +637,8 @@ export function renderCodexPetFrame(
624
637
  if (!frame) return [];
625
638
  const imageProtocol = getCapabilities().images;
626
639
  if (imageProtocol === "kitty") {
627
- return (options.kittyManager ?? defaultKittyManager(options.imageId)).renderFrame(
628
- frame,
629
- width,
630
- {
631
- sizeCells: options.sizeCells,
632
- },
633
- );
640
+ const manager = options.kittyManager ?? defaultKittyManager(options.imageId);
641
+ return manager.renderFrame(frame, width, { sizeCells: options.sizeCells });
634
642
  }
635
643
  if (!imageProtocol) {
636
644
  const fallback = imageFallback(frame.mimeType, {
@@ -684,6 +692,15 @@ function petCompletionValue(subcommand: string, pet: CodexPetPackage): string {
684
692
  return `${subcommand} ${pet.slug}`;
685
693
  }
686
694
 
695
+ function petContainsLookup(pet: CodexPetPackage, query: string): boolean {
696
+ return (
697
+ !query ||
698
+ petLookupKey(pet.slug).includes(query) ||
699
+ (pet.id !== undefined && petLookupKey(pet.id).includes(query)) ||
700
+ petLookupKey(pet.name).includes(query)
701
+ );
702
+ }
703
+
687
704
  export async function openAIPetsArgumentCompletions(
688
705
  argumentPrefix: string,
689
706
  home = codexHome(),
@@ -704,14 +721,7 @@ export async function openAIPetsArgumentCompletions(
704
721
  const petQuery = hasTrailingSpace ? "" : parts.slice(1).join(" ");
705
722
  const normalizedPetQuery = petLookupKey(petQuery);
706
723
  const pets = (await listCodexPets(home)).filter((pet) => pet.hasSpritesheet);
707
- const matches = pets.filter((pet) => {
708
- if (!normalizedPetQuery) return true;
709
- return (
710
- petLookupKey(pet.slug).includes(normalizedPetQuery) ||
711
- (pet.id !== undefined && petLookupKey(pet.id).includes(normalizedPetQuery)) ||
712
- petLookupKey(pet.name).includes(normalizedPetQuery)
713
- );
714
- });
724
+ const matches = pets.filter((pet) => petContainsLookup(pet, normalizedPetQuery));
715
725
  return matches.length > 0
716
726
  ? matches.map((pet) => ({
717
727
  value: petCompletionValue(subcommand, pet),
@@ -721,17 +731,20 @@ export async function openAIPetsArgumentCompletions(
721
731
  : null;
722
732
  }
723
733
 
734
+ function codexPetStatus(pet: CodexPetPackage): string {
735
+ if (pet.hasSpritesheet) return "ready";
736
+ return pet.spritesheetIssue ?? `missing ${pet.spritesheetPath}`;
737
+ }
738
+
739
+ function formatCodexPetListLine(pet: CodexPetPackage): string {
740
+ const description = pet.description ? `\n ${pet.description}` : "";
741
+ return `${pet.name} (${pet.slug}) — ${codexPetStatus(pet)}${description}`;
742
+ }
743
+
724
744
  export function formatCodexPetsListMessage(pets: CodexPetPackage[], home = codexHome()): string {
725
745
  if (pets.length === 0) return formatNoReadyCodexPetsMessage(pets, home);
726
746
 
727
- const petLines = pets
728
- .map((pet) => {
729
- const status = pet.hasSpritesheet
730
- ? "ready"
731
- : (pet.spritesheetIssue ?? `missing ${pet.spritesheetPath}`);
732
- return `${pet.name} (${pet.slug}) — ${status}${pet.description ? `\n ${pet.description}` : ""}`;
733
- })
734
- .join("\n");
747
+ const petLines = pets.map(formatCodexPetListLine).join("\n");
735
748
 
736
749
  if (pets.some((pet) => pet.hasSpritesheet)) return petLines;
737
750
 
@@ -763,25 +776,31 @@ export function registerOpenAIPets(pi: ExtensionAPI, controller: PetsCommandCont
763
776
  const [subcommand = "help", ...rest] = args.trim().split(/\s+/).filter(Boolean);
764
777
  const normalized = subcommand.toLowerCase();
765
778
  const slug = rest.join(" ").trim() || undefined;
766
- if (normalized === "help") {
767
- ctx.ui.notify(formatCodexPetsHelp(), "info");
768
- return;
769
- }
770
- if (normalized === "list") {
771
- await notifyPetsList(ctx);
772
- return;
773
- }
774
- if (normalized === "wake" && controller.wake) {
775
- await controller.wake(ctx, slug);
776
- return;
777
- }
778
- if (normalized === "tuck" && controller.tuck) {
779
- await controller.tuck(ctx);
780
- return;
781
- }
782
- if (normalized === "select" && controller.select) {
783
- await controller.select(ctx, slug);
784
- return;
779
+ switch (normalized) {
780
+ case "help":
781
+ ctx.ui.notify(formatCodexPetsHelp(), "info");
782
+ return;
783
+ case "list":
784
+ await notifyPetsList(ctx);
785
+ return;
786
+ case "wake":
787
+ if (controller.wake) {
788
+ await controller.wake(ctx, slug);
789
+ return;
790
+ }
791
+ break;
792
+ case "tuck":
793
+ if (controller.tuck) {
794
+ await controller.tuck(ctx);
795
+ return;
796
+ }
797
+ break;
798
+ case "select":
799
+ if (controller.select) {
800
+ await controller.select(ctx, slug);
801
+ return;
802
+ }
803
+ break;
785
804
  }
786
805
  ctx.ui.notify(`Usage: /${PETS_COMMAND} [help|list|wake [slug]|tuck|select <slug>]`, "error");
787
806
  },