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.
- package/package.json +1 -1
- package/src/pets.ts +110 -91
package/package.json
CHANGED
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:
|
|
113
|
-
|
|
114
|
-
const
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
|
228
|
-
if (!
|
|
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
|
|
237
|
-
if (!
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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}
|
|
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(
|
|
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:
|
|
389
|
-
heightPx:
|
|
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 *
|
|
396
|
-
top: animation.row *
|
|
397
|
-
width:
|
|
398
|
-
height:
|
|
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:
|
|
415
|
-
heightPx:
|
|
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]
|
|
462
|
+
return Math.max(1, Math.ceil(frames[0].durationMs));
|
|
456
463
|
}
|
|
457
464
|
|
|
458
|
-
function
|
|
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
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
|
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
|
|
508
|
-
const
|
|
509
|
-
if (
|
|
510
|
-
else chunks.push(`\x1b_Gm=${
|
|
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(
|
|
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
|
-
|
|
628
|
-
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
},
|