@lebronj/pi-suite 0.1.1 → 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
@@ -12,10 +12,10 @@ pi install npm:pi-mcp-adapter
12
12
  pi install npm:pi-subagents
13
13
  ```
14
14
 
15
- Or use the bootstrap script to install Pi, configure the team OpenAI-compatible endpoint, install this suite, and optionally set up qmd for memory search:
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
- bash scripts/bootstrap.sh
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.1",
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,21 +65,76 @@ NODE
65
65
  echo "Installing Pi extension suite: $PI_SUITE"
66
66
  pi install "$PI_SUITE"
67
67
 
68
- echo "Setting up qmd for memory_search when possible..."
69
- if command -v bun >/dev/null 2>&1; then
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
+
103
+ ensure_bun() {
104
+ if command -v bun >/dev/null 2>&1; then
105
+ return 0
106
+ fi
107
+ if ! command -v curl >/dev/null 2>&1; then
108
+ echo "curl is required to auto-install Bun for qmd." >&2
109
+ return 1
110
+ fi
111
+
112
+ echo "Installing Bun for qmd..."
113
+ curl -fsSL https://bun.sh/install | bash
114
+ export BUN_INSTALL="${BUN_INSTALL:-$HOME/.bun}"
115
+ export PATH="$BUN_INSTALL/bin:$PATH"
116
+
117
+ command -v bun >/dev/null 2>&1
118
+ }
119
+
120
+ echo "Setting up qmd for memory_search..."
121
+ if ensure_bun; then
70
122
  bun install -g https://github.com/tobi/qmd
71
- export PATH="$HOME/.bun/bin:$PATH"
123
+ export BUN_INSTALL="${BUN_INSTALL:-$HOME/.bun}"
124
+ export PATH="$BUN_INSTALL/bin:$PATH"
72
125
  mkdir -p "$HOME/.pi/agent/memory"
73
126
  if command -v qmd >/dev/null 2>&1; then
74
127
  qmd collection add "$HOME/.pi/agent/memory" --name pi-memory || true
75
- qmd embed || true
128
+ qmd embed || echo "qmd embed failed; run 'qmd embed' later to enable semantic memory_search."
76
129
  else
77
130
  echo "qmd was installed but is not on PATH. Add ~/.bun/bin to PATH, then run qmd embed."
78
131
  fi
79
132
  else
80
133
  cat <<'MSG'
81
- Bun not found. Core memory tools still work, but memory_search needs qmd.
82
- Install qmd later with:
134
+ Could not auto-install Bun, so qmd setup was skipped.
135
+ Core memory tools still work, but memory_search needs qmd.
136
+ Install later with:
137
+ curl -fsSL https://bun.sh/install | bash
83
138
  bun install -g https://github.com/tobi/qmd
84
139
  qmd collection add ~/.pi/agent/memory --name pi-memory
85
140
  qmd embed
@@ -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