@lebronj/pi-suite 0.1.2 → 0.1.4
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
|
@@ -15,7 +15,7 @@ pi install npm:pi-subagents
|
|
|
15
15
|
Or use the bootstrap script to install Pi, configure the team OpenAI-compatible endpoint, install this suite, and set up Bun + qmd for memory search:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
curl -fsSL https://registry.npmjs.org/@lebronj/pi-suite/-/pi-suite-0.1.
|
|
18
|
+
curl -fsSL https://registry.npmjs.org/@lebronj/pi-suite/-/pi-suite-0.1.3.tgz | tar -xzO package/scripts/bootstrap.sh | bash
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## What Is Included
|
package/extensions/pet.ts
CHANGED
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
serializeConversation,
|
|
34
34
|
type Theme,
|
|
35
35
|
} from "@earendil-works/pi-coding-agent";
|
|
36
|
-
import type { Component, OverlayHandle, TUI } from "@earendil-works/pi-tui";
|
|
36
|
+
import type { AutocompleteItem, Component, OverlayHandle, TUI } from "@earendil-works/pi-tui";
|
|
37
37
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
38
38
|
|
|
39
39
|
type PetMood = "idle" | "thinking" | "tool" | "chat" | "celebrate" | "concerned";
|
|
@@ -58,6 +58,18 @@ interface PetInventoryItem {
|
|
|
58
58
|
count: number;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
interface PetDropPity {
|
|
62
|
+
tool: number;
|
|
63
|
+
memory: number;
|
|
64
|
+
checkin: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface PetCommandCompletion {
|
|
68
|
+
name: string;
|
|
69
|
+
description: string;
|
|
70
|
+
usage?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
61
73
|
interface PetStats {
|
|
62
74
|
focus: number;
|
|
63
75
|
energy: number;
|
|
@@ -81,6 +93,7 @@ interface PetProfile {
|
|
|
81
93
|
lastCheckInDay?: string;
|
|
82
94
|
feedCount: number;
|
|
83
95
|
inventory: PetInventoryItem[];
|
|
96
|
+
itemDropPity: PetDropPity;
|
|
84
97
|
equippedItemId?: string;
|
|
85
98
|
}
|
|
86
99
|
|
|
@@ -99,11 +112,32 @@ const PET_ITEMS: PetItem[] = [
|
|
|
99
112
|
{ id: "violet-badge", name: "Violet Badge", rarity: "epic", glyph: "#", color: "error", trigger: "memory" },
|
|
100
113
|
{ id: "star-crown", name: "Star Crown", rarity: "legendary", glyph: "^", color: "warning", trigger: "memory" },
|
|
101
114
|
];
|
|
115
|
+
const PET_COMMAND_COMPLETIONS: PetCommandCompletion[] = [
|
|
116
|
+
{ name: "on", description: "Show the pet" },
|
|
117
|
+
{ name: "off", description: "Hide the pet" },
|
|
118
|
+
{ name: "cat", description: "Switch species to cat" },
|
|
119
|
+
{ name: "dog", description: "Switch species to dog" },
|
|
120
|
+
{ name: "fox", description: "Switch species to fox" },
|
|
121
|
+
{ name: "bot", description: "Switch species to bot" },
|
|
122
|
+
{ name: "name", usage: "<name>", description: "Rename the pet" },
|
|
123
|
+
{ name: "mood", description: "Show mood, stats, and progress" },
|
|
124
|
+
{ name: "checkin", description: "Claim the daily check-in reward" },
|
|
125
|
+
{ name: "feed", description: "Feed the pet" },
|
|
126
|
+
{ name: "bag", description: "Show inventory items" },
|
|
127
|
+
{ name: "inventory", description: "Show inventory items" },
|
|
128
|
+
{ name: "equip", usage: "<item>", description: "Equip a cosmetic item" },
|
|
129
|
+
{ name: "unequip", description: "Remove the equipped item" },
|
|
130
|
+
{ name: "position", usage: "widget|overlay", description: "Move between editor widget and floating overlay" },
|
|
131
|
+
{ name: "reset", description: "Reset pet profile" },
|
|
132
|
+
{ name: "ask", usage: "<question>", description: "Ask about current context without saving the answer" },
|
|
133
|
+
];
|
|
102
134
|
const DAILY_CHECKIN_XP = 2;
|
|
103
135
|
const FEED_XP = 1;
|
|
104
|
-
const TOOL_DROP_CHANCE = 0.
|
|
105
|
-
const MEMORY_DROP_CHANCE = 0.
|
|
106
|
-
const CHECKIN_DROP_CHANCE = 0.
|
|
136
|
+
const TOOL_DROP_CHANCE = 0.04;
|
|
137
|
+
const MEMORY_DROP_CHANCE = 0.12;
|
|
138
|
+
const CHECKIN_DROP_CHANCE = 0.2;
|
|
139
|
+
const DROP_PITY_LIMITS: Record<PetItemTrigger, number> = { tool: 25, memory: 8, checkin: 7 };
|
|
140
|
+
const RARITY_WEIGHTS: Record<PetItemRarity, number> = { common: 80, rare: 16, epic: 3.5, legendary: 0.5 };
|
|
107
141
|
|
|
108
142
|
const PET_ASK_PROMPT = `You are a tiny terminal pet companion inside pi.
|
|
109
143
|
Answer the user's question using the provided conversation context.
|
|
@@ -336,6 +370,7 @@ function defaultProfile(): PetProfile {
|
|
|
336
370
|
enabled: true,
|
|
337
371
|
feedCount: 0,
|
|
338
372
|
inventory: [],
|
|
373
|
+
itemDropPity: { tool: 0, memory: 0, checkin: 0 },
|
|
339
374
|
};
|
|
340
375
|
}
|
|
341
376
|
|
|
@@ -357,6 +392,7 @@ function normalizeProfile(value: unknown): PetProfile | undefined {
|
|
|
357
392
|
typeof partial.equippedItemId === "string" && PET_ITEMS.some((item) => item.id === partial.equippedItemId)
|
|
358
393
|
? partial.equippedItemId
|
|
359
394
|
: undefined;
|
|
395
|
+
const itemDropPity = normalizeDropPity(partial.itemDropPity, fallback.itemDropPity);
|
|
360
396
|
|
|
361
397
|
return {
|
|
362
398
|
name: typeof partial.name === "string" && partial.name.trim() ? partial.name.trim().slice(0, 24) : fallback.name,
|
|
@@ -379,10 +415,20 @@ function normalizeProfile(value: unknown): PetProfile | undefined {
|
|
|
379
415
|
lastCheckInDay: typeof partial.lastCheckInDay === "string" ? partial.lastCheckInDay : undefined,
|
|
380
416
|
feedCount: safeCount(partial.feedCount, fallback.feedCount),
|
|
381
417
|
inventory,
|
|
418
|
+
itemDropPity,
|
|
382
419
|
equippedItemId,
|
|
383
420
|
};
|
|
384
421
|
}
|
|
385
422
|
|
|
423
|
+
function normalizeDropPity(value: unknown, fallback: PetDropPity): PetDropPity {
|
|
424
|
+
const partial = value && typeof value === "object" ? (value as Partial<PetDropPity>) : {};
|
|
425
|
+
return {
|
|
426
|
+
tool: safeCount(partial.tool, fallback.tool),
|
|
427
|
+
memory: safeCount(partial.memory, fallback.memory),
|
|
428
|
+
checkin: safeCount(partial.checkin, fallback.checkin),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
386
432
|
function normalizeInventoryItem(value: unknown): PetInventoryItem | undefined {
|
|
387
433
|
if (!value || typeof value !== "object") return undefined;
|
|
388
434
|
const partial = value as Partial<PetInventoryItem>;
|
|
@@ -461,6 +507,7 @@ function createStatusCard(profile: PetProfile): string {
|
|
|
461
507
|
`stats: focus ${profile.stats.focus}/10, energy ${profile.stats.energy}/10, curiosity ${profile.stats.curiosity}/10`,
|
|
462
508
|
` sass ${profile.stats.sass}/10, loyalty ${profile.stats.loyalty}/10`,
|
|
463
509
|
`activity: ${profile.interactions} chats, ${profile.toolsCompleted} tools, feeds ${profile.feedCount}`,
|
|
510
|
+
`drops: tools ${profile.itemDropPity.tool}/${DROP_PITY_LIMITS.tool}, memory ${profile.itemDropPity.memory}/${DROP_PITY_LIMITS.memory}, check-in ${profile.itemDropPity.checkin}/${DROP_PITY_LIMITS.checkin}`,
|
|
464
511
|
`bag: ${itemCount} items${equipped ? `, equipped ${equipped.name}` : ""} check-in: ${profile.lastCheckInDay ?? "never"}`,
|
|
465
512
|
].join("\n");
|
|
466
513
|
}
|
|
@@ -550,8 +597,11 @@ async function askPet(question: string, ctx: ExtensionCommandContext): Promise<s
|
|
|
550
597
|
|
|
551
598
|
const generate = async () => {
|
|
552
599
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model!);
|
|
553
|
-
if (
|
|
554
|
-
throw new Error(auth.
|
|
600
|
+
if (auth.ok === false) {
|
|
601
|
+
throw new Error(auth.error);
|
|
602
|
+
}
|
|
603
|
+
if (!auth.apiKey) {
|
|
604
|
+
throw new Error(`No API key for ${ctx.model!.provider}`);
|
|
555
605
|
}
|
|
556
606
|
|
|
557
607
|
const conversationText = serializeConversation(convertToLlm(contextMessages));
|
|
@@ -688,17 +738,29 @@ export default function petExtension(pi: ExtensionAPI) {
|
|
|
688
738
|
else profile.inventory.push({ itemId: item.id, count: 1 });
|
|
689
739
|
}
|
|
690
740
|
|
|
741
|
+
function rollWeightedItem(candidates: PetItem[]): PetItem | undefined {
|
|
742
|
+
const weighted = candidates.map((item) => ({ item, weight: RARITY_WEIGHTS[item.rarity] ?? 1 }));
|
|
743
|
+
const totalWeight = weighted.reduce((total, entry) => total + entry.weight, 0);
|
|
744
|
+
if (totalWeight <= 0) return candidates[0];
|
|
745
|
+
|
|
746
|
+
let roll = Math.random() * totalWeight;
|
|
747
|
+
for (const entry of weighted) {
|
|
748
|
+
roll -= entry.weight;
|
|
749
|
+
if (roll <= 0) return entry.item;
|
|
750
|
+
}
|
|
751
|
+
return weighted[weighted.length - 1]?.item;
|
|
752
|
+
}
|
|
753
|
+
|
|
691
754
|
function rollDrop(trigger: PetItemTrigger, chance: number): PetItem | undefined {
|
|
692
|
-
|
|
755
|
+
profile.itemDropPity[trigger] = safeCount(profile.itemDropPity[trigger], 0) + 1;
|
|
756
|
+
const forced = profile.itemDropPity[trigger] >= DROP_PITY_LIMITS[trigger];
|
|
757
|
+
if (!forced && Math.random() >= chance) return undefined;
|
|
758
|
+
|
|
693
759
|
const candidates = PET_ITEMS.filter((item) => item.trigger === trigger);
|
|
694
760
|
if (candidates.length === 0) return undefined;
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
const rarityOrder: PetItemRarity[] = ["common", "rare", "epic", "legendary"];
|
|
699
|
-
const maxIndex = rarityOrder.indexOf(maxRarity);
|
|
700
|
-
const pool = candidates.filter((item) => rarityOrder.indexOf(item.rarity) <= maxIndex);
|
|
701
|
-
return pool[Math.floor(Math.random() * pool.length)];
|
|
761
|
+
const item = rollWeightedItem(candidates);
|
|
762
|
+
if (item) profile.itemDropPity[trigger] = 0;
|
|
763
|
+
return item;
|
|
702
764
|
}
|
|
703
765
|
|
|
704
766
|
function maybeDropItem(trigger: PetItemTrigger, chance: number): PetItem | undefined {
|
|
@@ -706,9 +768,71 @@ export default function petExtension(pi: ExtensionAPI) {
|
|
|
706
768
|
if (!item) return undefined;
|
|
707
769
|
addItem(item);
|
|
708
770
|
setReply(`Found ${item.rarity} item: ${item.name} ${item.glyph}`, "celebrate", 7000);
|
|
771
|
+
saveProfile();
|
|
709
772
|
return item;
|
|
710
773
|
}
|
|
711
774
|
|
|
775
|
+
function getPetArgumentCompletions(argumentPrefix: string): AutocompleteItem[] | null {
|
|
776
|
+
const trimmed = argumentPrefix.trimStart();
|
|
777
|
+
const firstSpace = trimmed.indexOf(" ");
|
|
778
|
+
|
|
779
|
+
if (firstSpace === -1) {
|
|
780
|
+
const lower = trimmed.toLowerCase();
|
|
781
|
+
const matches = PET_COMMAND_COMPLETIONS.filter((command) => command.name.startsWith(lower)).map((command) => ({
|
|
782
|
+
value: command.usage ? `${command.name} ` : command.name,
|
|
783
|
+
label: command.usage ? `${command.name} ${command.usage}` : command.name,
|
|
784
|
+
description: command.description,
|
|
785
|
+
}));
|
|
786
|
+
return matches.length > 0 ? matches : null;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const subcommand = trimmed.slice(0, firstSpace).toLowerCase();
|
|
790
|
+
const subArg = trimmed
|
|
791
|
+
.slice(firstSpace + 1)
|
|
792
|
+
.trimStart()
|
|
793
|
+
.toLowerCase();
|
|
794
|
+
|
|
795
|
+
const usageCompletion = PET_COMMAND_COMPLETIONS.find((command) => command.name === subcommand && command.usage);
|
|
796
|
+
if (usageCompletion && subArg.length === 0 && subcommand !== "position" && subcommand !== "equip") {
|
|
797
|
+
return [
|
|
798
|
+
{
|
|
799
|
+
value: `${subcommand} `,
|
|
800
|
+
label: usageCompletion.usage ?? "",
|
|
801
|
+
description: usageCompletion.description,
|
|
802
|
+
},
|
|
803
|
+
];
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (subcommand === "position") {
|
|
807
|
+
return filterPetCompletions(subArg, [
|
|
808
|
+
{ value: "position widget", label: "widget", description: "Show pet above the editor" },
|
|
809
|
+
{ value: "position overlay", label: "overlay", description: "Show pet as a bottom-right overlay" },
|
|
810
|
+
]);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (subcommand === "equip") {
|
|
814
|
+
const items = profile.inventory
|
|
815
|
+
.map((owned) => getPetItem(owned.itemId))
|
|
816
|
+
.filter((item) => item !== undefined)
|
|
817
|
+
.map((item) => ({
|
|
818
|
+
value: `equip ${item.id}`,
|
|
819
|
+
label: item.name,
|
|
820
|
+
description: `${item.rarity} item (${item.id})`,
|
|
821
|
+
}));
|
|
822
|
+
return filterPetCompletions(subArg, items);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function filterPetCompletions(prefix: string, items: AutocompleteItem[]): AutocompleteItem[] | null {
|
|
829
|
+
const matches = items.filter((item) => {
|
|
830
|
+
const text = `${item.value} ${item.label} ${item.description ?? ""}`.toLowerCase();
|
|
831
|
+
return text.includes(prefix);
|
|
832
|
+
});
|
|
833
|
+
return matches.length > 0 ? matches : null;
|
|
834
|
+
}
|
|
835
|
+
|
|
712
836
|
function createBagCard(): string {
|
|
713
837
|
if (profile.inventory.length === 0) return "Bag is empty. Try /pet checkin or keep working with tools.";
|
|
714
838
|
return profile.inventory
|
|
@@ -865,6 +989,7 @@ export default function petExtension(pi: ExtensionAPI) {
|
|
|
865
989
|
|
|
866
990
|
pi.registerCommand("pet", {
|
|
867
991
|
description: "Show, hide, switch, talk to, or ask the terminal pet about current context.",
|
|
992
|
+
getArgumentCompletions: getPetArgumentCompletions,
|
|
868
993
|
handler: async (args, ctx) => {
|
|
869
994
|
const raw = args.trim();
|
|
870
995
|
const next = raw.toLowerCase();
|
package/package.json
CHANGED
package/scripts/bootstrap.sh
CHANGED
|
@@ -65,6 +65,41 @@ NODE
|
|
|
65
65
|
echo "Installing Pi extension suite: $PI_SUITE"
|
|
66
66
|
pi install "$PI_SUITE"
|
|
67
67
|
|
|
68
|
+
link_if_safe() {
|
|
69
|
+
local source_path="$1"
|
|
70
|
+
local link_path="$2"
|
|
71
|
+
local label="$3"
|
|
72
|
+
|
|
73
|
+
if [ ! -e "$source_path" ]; then
|
|
74
|
+
echo "Skip linking $label: source does not exist: $source_path"
|
|
75
|
+
return 0
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
mkdir -p "$(dirname "$link_path")"
|
|
79
|
+
if [ -L "$link_path" ]; then
|
|
80
|
+
ln -sfn "$source_path" "$link_path"
|
|
81
|
+
echo "Linked $label: $link_path -> $source_path"
|
|
82
|
+
elif [ -d "$link_path" ] && [ -z "$(find "$link_path" -mindepth 1 -maxdepth 1 -print -quit)" ]; then
|
|
83
|
+
rmdir "$link_path"
|
|
84
|
+
ln -s "$source_path" "$link_path"
|
|
85
|
+
echo "Linked $label: $link_path -> $source_path"
|
|
86
|
+
elif [ ! -e "$link_path" ]; then
|
|
87
|
+
ln -s "$source_path" "$link_path"
|
|
88
|
+
echo "Linked $label: $link_path -> $source_path"
|
|
89
|
+
else
|
|
90
|
+
echo "Skip linking $label: $link_path exists and is not empty."
|
|
91
|
+
fi
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
WORKSPACE_DIR="${PI_WORKSPACE_DIR:-$PWD}"
|
|
95
|
+
WORKSPACE_PI_DIR="$WORKSPACE_DIR/.pi"
|
|
96
|
+
MEMORY_DIR="$AGENT_DIR/memory"
|
|
97
|
+
SUITE_SKILLS_DIR="${PI_SUITE_SKILLS_DIR:-$AGENT_DIR/npm/node_modules/@lebronj/pi-suite/skills}"
|
|
98
|
+
|
|
99
|
+
mkdir -p "$MEMORY_DIR"
|
|
100
|
+
link_if_safe "$MEMORY_DIR" "$WORKSPACE_PI_DIR/memory" "memory"
|
|
101
|
+
link_if_safe "$SUITE_SKILLS_DIR" "$WORKSPACE_PI_DIR/skills" "skills"
|
|
102
|
+
|
|
68
103
|
ensure_bun() {
|
|
69
104
|
if command -v bun >/dev/null 2>&1; then
|
|
70
105
|
return 0
|
package/skills/pi-skill/SKILL.md
CHANGED
|
@@ -140,8 +140,9 @@ Current project packages:
|
|
|
140
140
|
- `pi-subagents`
|
|
141
141
|
- `pi-lens`
|
|
142
142
|
|
|
143
|
-
Project extensions are in `.pi/extensions/`. Important current
|
|
143
|
+
Project extensions are in `.pi/extensions/`. Important current extensions:
|
|
144
144
|
|
|
145
|
+
- `pet.ts`: terminal pet UI with `/pet` subcommands, argument autocomplete for actions such as `/pet ask`, `/pet position`, and `/pet equip`, plus item drops with pity counters.
|
|
145
146
|
- `memory-curator.ts`: deprecated compatibility notice only. The external curator service is managed by `@jhp/pi-memory`.
|
|
146
147
|
|
|
147
148
|
## Recommended Workflows
|
|
File without changes
|
|
File without changes
|