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