pi-better-openai 0.1.16 → 0.1.18

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 +123 -92
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.18",
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
 
@@ -215,6 +220,18 @@ function petLookupKey(value: string): string {
215
220
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
216
221
  }
217
222
 
223
+ type PetLookupRequest = {
224
+ hasText: boolean;
225
+ key?: string;
226
+ };
227
+
228
+ function petLookupRequest(value?: string): PetLookupRequest {
229
+ const requestedText = value?.trim();
230
+ if (!requestedText) return { hasText: false };
231
+
232
+ return { hasText: true, key: petLookupKey(requestedText) || undefined };
233
+ }
234
+
218
235
  function petMatchesLookup(pet: CodexPetPackage, requested: string): boolean {
219
236
  return (
220
237
  petLookupKey(pet.slug) === requested ||
@@ -224,19 +241,22 @@ function petMatchesLookup(pet: CodexPetPackage, requested: string): boolean {
224
241
  }
225
242
 
226
243
  export function findCodexPet(pets: CodexPetPackage[], value?: string): CodexPetPackage | undefined {
227
- const requested = value?.trim() ? petLookupKey(value.trim()) : "";
228
- if (!requested) return undefined;
229
- return pets.find((pet) => petMatchesLookup(pet, requested));
244
+ const request = petLookupRequest(value);
245
+ const { key } = request;
246
+ if (!key) return undefined;
247
+ return pets.find((pet) => petMatchesLookup(pet, key));
230
248
  }
231
249
 
232
250
  export function findReadyCodexPet(
233
251
  pets: CodexPetPackage[],
234
252
  value?: string,
235
253
  ): 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));
254
+ const request = petLookupRequest(value);
255
+ if (!request.hasText) return pets.find((pet) => pet.hasSpritesheet);
256
+
257
+ const { key } = request;
258
+ if (!key) return undefined;
259
+ return pets.find((pet) => pet.hasSpritesheet && petMatchesLookup(pet, key));
240
260
  }
241
261
 
242
262
  function selectPet(pets: CodexPetPackage[], slug?: string): CodexPetPackage | undefined {
@@ -255,7 +275,7 @@ export function formatCodexPetSetupInstructions(home = codexHome()): string {
255
275
  "",
256
276
  "Expected files:",
257
277
  " pet.json",
258
- " spritesheet.webp",
278
+ ` ${DEFAULT_SPRITESHEET_PATH}`,
259
279
  "",
260
280
  "Create one:",
261
281
  " $skill-installer hatch-pet",
@@ -323,7 +343,7 @@ export function formatNoReadyCodexPetsMessage(pets: CodexPetPackage[], home = co
323
343
  return [
324
344
  "Found custom Codex pets, but none are ready.",
325
345
  "",
326
- `A ready pet needs pet.json and a readable ${EXPECTED_ATLAS_WIDTH}x${EXPECTED_ATLAS_HEIGHT} spritesheet.webp atlas in its pet folder.`,
346
+ `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
347
  "Run /pets list to see what needs fixing, or create a new pet:",
328
348
  "",
329
349
  formatCodexPetSetupInstructions(home),
@@ -354,8 +374,6 @@ export async function loadCodexPet(
354
374
  `Invalid Codex pet atlas dimensions: ${metadata.width ?? "?"}x${metadata.height ?? "?"}; expected ${EXPECTED_ATLAS_WIDTH}x${EXPECTED_ATLAS_HEIGHT}.`,
355
375
  );
356
376
  }
357
- const cellWidth = DEFAULT_CELL_WIDTH;
358
- const cellHeight = DEFAULT_CELL_HEIGHT;
359
377
  const states = {} as Record<PetState, PetFrame[]>;
360
378
  const imageProtocol = getCapabilities().images;
361
379
  const useKitty = imageProtocol === "kitty";
@@ -368,10 +386,13 @@ export async function loadCodexPet(
368
386
  const targetHeightPx = targetWidthPx
369
387
  ? Math.max(
370
388
  1,
371
- Math.ceil((cellHeight * targetWidthPx) / cellWidth / cellDimensions.heightPx) *
372
- cellDimensions.heightPx,
389
+ Math.ceil(
390
+ (DEFAULT_CELL_HEIGHT * targetWidthPx) / DEFAULT_CELL_WIDTH / cellDimensions.heightPx,
391
+ ) * cellDimensions.heightPx,
373
392
  )
374
393
  : undefined;
394
+ const outputWidthPx = targetWidthPx ?? DEFAULT_CELL_WIDTH;
395
+ const outputHeightPx = targetHeightPx ?? DEFAULT_CELL_HEIGHT;
375
396
 
376
397
  for (const [state, animation] of Object.entries(PET_ANIMATION_ROWS) as Array<
377
398
  [PetState, (typeof PET_ANIMATION_ROWS)[PetState]]
@@ -379,23 +400,21 @@ export async function loadCodexPet(
379
400
  states[state] = [];
380
401
  for (let column = 0; column < animation.durations.length; column++) {
381
402
  const durationMs = animation.durations[column] ?? 150;
382
- const frameWidthPx = targetWidthPx ?? cellWidth;
383
- const frameHeightPx = targetHeightPx ?? cellHeight;
384
403
  if (!imageProtocol) {
385
404
  states[state].push({
386
405
  mimeType: "image/png",
387
406
  durationMs,
388
- widthPx: frameWidthPx,
389
- heightPx: frameHeightPx,
407
+ widthPx: outputWidthPx,
408
+ heightPx: outputHeightPx,
390
409
  });
391
410
  continue;
392
411
  }
393
412
 
394
413
  let frame = source.clone().extract({
395
- left: column * cellWidth,
396
- top: animation.row * cellHeight,
397
- width: cellWidth,
398
- height: cellHeight,
414
+ left: column * DEFAULT_CELL_WIDTH,
415
+ top: animation.row * DEFAULT_CELL_HEIGHT,
416
+ width: DEFAULT_CELL_WIDTH,
417
+ height: DEFAULT_CELL_HEIGHT,
399
418
  });
400
419
  if (targetWidthPx && targetHeightPx) {
401
420
  frame = frame.resize(targetWidthPx, targetHeightPx, {
@@ -411,8 +430,8 @@ export async function loadCodexPet(
411
430
  kittyImageId: useKitty ? kittyImageBase + kittyFrameOffset++ : undefined,
412
431
  mimeType: "image/png",
413
432
  durationMs,
414
- widthPx: frameWidthPx,
415
- heightPx: frameHeightPx,
433
+ widthPx: outputWidthPx,
434
+ heightPx: outputHeightPx,
416
435
  });
417
436
  }
418
437
  }
@@ -452,24 +471,30 @@ export function nextAnimationFrameDelayMs(
452
471
  if (cursor < duration) return Math.max(1, Math.ceil(duration - cursor));
453
472
  cursor -= duration;
454
473
  }
455
- return Math.max(1, Math.ceil(frames[0]?.durationMs ?? 120));
474
+ return Math.max(1, Math.ceil(frames[0].durationMs));
456
475
  }
457
476
 
458
- function markCodexPetKittyFramesUnuploaded(pet?: LoadedCodexPet): void {
477
+ function forEachCodexPetFrame(
478
+ pet: LoadedCodexPet | undefined,
479
+ visit: (frame: PetFrame) => void,
480
+ ): void {
459
481
  if (!pet) return;
460
482
  for (const frames of Object.values(pet.states)) {
461
- for (const frame of frames) frame.kittyUploaded = false;
483
+ for (const frame of frames) visit(frame);
462
484
  }
463
485
  }
464
486
 
487
+ function markCodexPetKittyFramesUnuploaded(pet?: LoadedCodexPet): void {
488
+ forEachCodexPetFrame(pet, (frame) => {
489
+ frame.kittyUploaded = false;
490
+ });
491
+ }
492
+
465
493
  function codexPetKittyImageIds(pet?: LoadedCodexPet): number[] {
466
- if (!pet) return [];
467
494
  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
- }
495
+ forEachCodexPetFrame(pet, (frame) => {
496
+ if (frame.kittyImageId !== undefined) imageIds.add(frame.kittyImageId);
497
+ });
473
498
  return Array.from(imageIds);
474
499
  }
475
500
 
@@ -496,18 +521,18 @@ function encodeKittyRawRgba(frame: PetFrame, imageId: number): string {
496
521
  `v=${frame.heightPx}`,
497
522
  `i=${imageId}`,
498
523
  "q=2",
499
- ];
524
+ ].join(",");
500
525
  if (rawRgbaData.length <= chunkSize) {
501
- return `\x1b_G${params.join(",")};${rawRgbaData}\x1b\\`;
526
+ return `\x1b_G${params};${rawRgbaData}\x1b\\`;
502
527
  }
503
528
 
504
529
  const chunks: string[] = [];
505
530
  for (let offset = 0; offset < rawRgbaData.length; offset += chunkSize) {
506
531
  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\\`);
532
+ const isFirstChunk = offset === 0;
533
+ const isLastChunk = offset + chunkSize >= rawRgbaData.length;
534
+ if (isFirstChunk) chunks.push(`\x1b_G${params},m=1;${chunk}\x1b\\`);
535
+ else chunks.push(`\x1b_Gm=${isLastChunk ? 0 : 1};${chunk}\x1b\\`);
511
536
  }
512
537
  return chunks.join("");
513
538
  }
@@ -549,7 +574,7 @@ export class CodexPetKittyManager {
549
574
  }
550
575
 
551
576
  renderFrame(frame: PetFrame, width: number, options: { sizeCells: number }): string[] {
552
- const columns = Math.max(1, Math.min(Math.max(1, width - 2), options.sizeCells));
577
+ const columns = Math.max(1, Math.min(width - 2, options.sizeCells));
553
578
  const rows = calculateImageRows(
554
579
  { widthPx: frame.widthPx, heightPx: frame.heightPx },
555
580
  columns,
@@ -624,13 +649,8 @@ export function renderCodexPetFrame(
624
649
  if (!frame) return [];
625
650
  const imageProtocol = getCapabilities().images;
626
651
  if (imageProtocol === "kitty") {
627
- return (options.kittyManager ?? defaultKittyManager(options.imageId)).renderFrame(
628
- frame,
629
- width,
630
- {
631
- sizeCells: options.sizeCells,
632
- },
633
- );
652
+ const manager = options.kittyManager ?? defaultKittyManager(options.imageId);
653
+ return manager.renderFrame(frame, width, { sizeCells: options.sizeCells });
634
654
  }
635
655
  if (!imageProtocol) {
636
656
  const fallback = imageFallback(frame.mimeType, {
@@ -684,6 +704,15 @@ function petCompletionValue(subcommand: string, pet: CodexPetPackage): string {
684
704
  return `${subcommand} ${pet.slug}`;
685
705
  }
686
706
 
707
+ function petContainsLookup(pet: CodexPetPackage, query: string): boolean {
708
+ return (
709
+ !query ||
710
+ petLookupKey(pet.slug).includes(query) ||
711
+ (pet.id !== undefined && petLookupKey(pet.id).includes(query)) ||
712
+ petLookupKey(pet.name).includes(query)
713
+ );
714
+ }
715
+
687
716
  export async function openAIPetsArgumentCompletions(
688
717
  argumentPrefix: string,
689
718
  home = codexHome(),
@@ -704,14 +733,7 @@ export async function openAIPetsArgumentCompletions(
704
733
  const petQuery = hasTrailingSpace ? "" : parts.slice(1).join(" ");
705
734
  const normalizedPetQuery = petLookupKey(petQuery);
706
735
  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
- });
736
+ const matches = pets.filter((pet) => petContainsLookup(pet, normalizedPetQuery));
715
737
  return matches.length > 0
716
738
  ? matches.map((pet) => ({
717
739
  value: petCompletionValue(subcommand, pet),
@@ -721,17 +743,20 @@ export async function openAIPetsArgumentCompletions(
721
743
  : null;
722
744
  }
723
745
 
746
+ function codexPetStatus(pet: CodexPetPackage): string {
747
+ if (pet.hasSpritesheet) return "ready";
748
+ return pet.spritesheetIssue ?? `missing ${pet.spritesheetPath}`;
749
+ }
750
+
751
+ function formatCodexPetListLine(pet: CodexPetPackage): string {
752
+ const description = pet.description ? `\n ${pet.description}` : "";
753
+ return `${pet.name} (${pet.slug}) — ${codexPetStatus(pet)}${description}`;
754
+ }
755
+
724
756
  export function formatCodexPetsListMessage(pets: CodexPetPackage[], home = codexHome()): string {
725
757
  if (pets.length === 0) return formatNoReadyCodexPetsMessage(pets, home);
726
758
 
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");
759
+ const petLines = pets.map(formatCodexPetListLine).join("\n");
735
760
 
736
761
  if (pets.some((pet) => pet.hasSpritesheet)) return petLines;
737
762
 
@@ -763,25 +788,31 @@ export function registerOpenAIPets(pi: ExtensionAPI, controller: PetsCommandCont
763
788
  const [subcommand = "help", ...rest] = args.trim().split(/\s+/).filter(Boolean);
764
789
  const normalized = subcommand.toLowerCase();
765
790
  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;
791
+ switch (normalized) {
792
+ case "help":
793
+ ctx.ui.notify(formatCodexPetsHelp(), "info");
794
+ return;
795
+ case "list":
796
+ await notifyPetsList(ctx);
797
+ return;
798
+ case "wake":
799
+ if (controller.wake) {
800
+ await controller.wake(ctx, slug);
801
+ return;
802
+ }
803
+ break;
804
+ case "tuck":
805
+ if (controller.tuck) {
806
+ await controller.tuck(ctx);
807
+ return;
808
+ }
809
+ break;
810
+ case "select":
811
+ if (controller.select) {
812
+ await controller.select(ctx, slug);
813
+ return;
814
+ }
815
+ break;
785
816
  }
786
817
  ctx.ui.notify(`Usage: /${PETS_COMMAND} [help|list|wake [slug]|tuck|select <slug>]`, "error");
787
818
  },