@lebronj/pi-suite 0.1.2 → 0.1.3

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.2.tgz | tar -xzO package/scripts/bootstrap.sh | bash
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.015;
105
- const MEMORY_DROP_CHANCE = 0.05;
106
- const CHECKIN_DROP_CHANCE = 0.08;
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 (!auth.ok || !auth.apiKey) {
554
- throw new Error(auth.ok ? `No API key for ${ctx.model!.provider}` : auth.error);
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
- if (Math.random() >= chance) return undefined;
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 roll = Math.random();
696
- const maxRarity: PetItemRarity =
697
- roll < 0.82 ? "common" : roll < 0.97 ? "rare" : roll < 0.995 ? "epic" : "legendary";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lebronj/pi-suite",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "JHP's Pi extension suite for team coding workflows",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -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
@@ -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 extension:
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