pi-agents-switch 0.3.0 → 0.3.2

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,6 +12,10 @@ pi install ~/project/agents-switch
12
12
 
13
13
  Then `/reload` in pi.
14
14
 
15
+ > **Requires Pi ≥ v0.x** (tested with the version shipping `before_agent_start` + `before_provider_request` events).
16
+ > If Pi's system prompt format changes, agent identity will be prepended instead of replacing the default role line —
17
+ > check the Pi console for a `[agents-switch]` warning if this happens.
18
+
15
19
  ## Usage
16
20
 
17
21
  | Action | How |
package/index.ts CHANGED
@@ -752,6 +752,12 @@ export default function agentsSwitch(pi: ExtensionAPI) {
752
752
  section +
753
753
  newPrompt.substring(roleEnd);
754
754
  } else {
755
+ // Pi version mismatch — role line changed. Fall back to prepend.
756
+ console.warn(
757
+ "[agents-switch] Default role line not found in system prompt. " +
758
+ "Pi may have changed its prompt format. " +
759
+ "Agent identity will be prepended instead of replacing the role line.",
760
+ );
755
761
  newPrompt = section + "\n\n" + newPrompt;
756
762
  }
757
763
  }
@@ -760,20 +766,35 @@ export default function agentsSwitch(pi: ExtensionAPI) {
760
766
  if (currentResolved && currentResolved.skillNames.length > 0) {
761
767
  const loadedSkills = (event.systemPromptOptions as { skills?: any[] })
762
768
  .skills;
763
- let selectedSkills: Array<{
764
- name: string;
765
- description: string;
766
- filePath: string;
767
- }>;
768
-
769
- if (loadedSkills && loadedSkills.length > 0) {
770
- selectedSkills = loadedSkills.filter((s) =>
771
- currentResolved!.skillNames.includes(s.name),
772
- );
773
- } else {
774
- selectedSkills = pm.getSkillObjects(currentResolved.skillNames);
769
+ const skillMap = new Map<
770
+ string,
771
+ { name: string; description: string; filePath: string }
772
+ >();
773
+
774
+ // Collect from Pi's loaded skills first (richest data)
775
+ if (loadedSkills) {
776
+ for (const s of loadedSkills) {
777
+ skillMap.set(s.name, {
778
+ name: s.name,
779
+ description: s.description ?? "",
780
+ filePath: s.filePath ?? "",
781
+ });
782
+ }
775
783
  }
776
784
 
785
+ // Fill gaps from disk (agent-specific skills not in Pi's loaded list)
786
+ for (const name of currentResolved.skillNames) {
787
+ if (!skillMap.has(name)) {
788
+ const found = pm.getSkillObjects([name]);
789
+ if (found.length > 0) skillMap.set(found[0].name, found[0]);
790
+ }
791
+ }
792
+
793
+ // Filter to only resolved names, preserving order
794
+ const selectedSkills = currentResolved.skillNames
795
+ .map((name) => skillMap.get(name))
796
+ .filter((s): s is NonNullable<typeof s> => s != null);
797
+
777
798
  if (selectedSkills.length > 0) {
778
799
  const skillsBlock = formatSkillsBlock(selectedSkills);
779
800
  const currentDateMarker = "\nCurrent date:";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agents-switch",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Tab to switch primary agents in Pi — like OpenCode's agent switching. Each agent gets an isolated profile with its own AGENTS.md, extensions, skills, and settings.",
5
5
  "type": "module",
6
6
  "pi": {
@@ -478,12 +478,10 @@ export class ProfileManager {
478
478
  * Look up full skill objects by name from Pi's standard skill directories.
479
479
  * Returns name, description, and filePath for each skill found.
480
480
  */
481
- /** Get PI skill names (global + extension + project, excluding agent-scoped). */
481
+ /** Get all indexed skill names (global + extension + project). */
482
482
  getPISkillNames(): string[] {
483
483
  if (!this.skillCache) this.rebuildSkillIndex();
484
- return [...this.skillCache!.entries()]
485
- .filter(([, v]) => v.source !== "agent")
486
- .map(([k]) => k);
484
+ return [...this.skillCache!.keys()];
487
485
  }
488
486
 
489
487
  /**
@@ -562,17 +560,76 @@ export class ProfileManager {
562
560
  if (!this.skillCache) {
563
561
  this.rebuildSkillIndex();
564
562
  }
565
- return this.skillCache!.get(name);
563
+ const cached = this.skillCache!.get(name);
564
+ if (cached) return cached;
565
+
566
+ // Fallback: agent-specific skills are not indexed globally.
567
+ // Check each agent's skills/ dir on demand.
568
+ return this.findAgentSkillOnDisk(name);
569
+ }
570
+
571
+ /**
572
+ * Look up a single agent-specific skill by checking each agent's
573
+ * skills/<name>/SKILL.md path directly.
574
+ */
575
+ private findAgentSkillOnDisk(
576
+ name: string,
577
+ ): { name: string; description: string; filePath: string } | undefined {
578
+ const read = (
579
+ filePath: string,
580
+ ): { name: string; description: string; filePath: string } | undefined => {
581
+ if (!existsSync(filePath)) return undefined;
582
+ try {
583
+ const content = readFileSync(filePath, "utf8");
584
+ const { frontmatter } = parseFrontmatter(content);
585
+ return {
586
+ name: frontmatter.name ?? name,
587
+ description: frontmatter.description ?? "",
588
+ filePath,
589
+ };
590
+ } catch {
591
+ return undefined;
592
+ }
593
+ };
594
+
595
+ // User-level agents
596
+ const userDir = join(homedir(), ".pi", "agents");
597
+ if (existsSync(userDir)) {
598
+ try {
599
+ for (const agent of readdirSync(userDir)) {
600
+ const found = read(join(userDir, agent, "skills", name, "SKILL.md"));
601
+ if (found) return found;
602
+ }
603
+ } catch {}
604
+ }
605
+
606
+ // Project-level agents
607
+ if (this.cwd) {
608
+ const projDir = join(this.cwd, ".pi", "agents");
609
+ if (existsSync(projDir)) {
610
+ try {
611
+ for (const agent of readdirSync(projDir)) {
612
+ const found = read(
613
+ join(projDir, agent, "skills", name, "SKILL.md"),
614
+ );
615
+ if (found) return found;
616
+ }
617
+ } catch {}
618
+ }
619
+ }
620
+
621
+ return undefined;
566
622
  }
567
623
 
568
624
  /**
569
- * Rebuild the skill index by scanning all known skill directories.
625
+ * Rebuild the skill index by scanning known skill directories.
626
+ * Agent-specific skills are NOT indexed here — they are looked up
627
+ * on demand via findSkillOnDisk → scanForAgentSkill().
570
628
  *
571
629
  * Locations scanned:
572
630
  * 1. ~/.pi/agent/skills/<name>/SKILL.md (flat, user-installed)
573
631
  * 2. ~/.pi/agent/npm/node_modules/<ext>/skills/ (extension skills)
574
- * 3. ~/.pi/agents/<agent>/skills/ (agent-specific skills)
575
- * 4. <cwd>/.pi/skills/ (project skills, flat)
632
+ * 3. <cwd>/.pi/skills/ (project skills, flat)
576
633
  */
577
634
  rebuildSkillIndex(): void {
578
635
  this.skillCache = new Map();
@@ -580,7 +637,6 @@ export class ProfileManager {
580
637
  const scanDir = (dir: string, maxDepth: number, source: string): void => {
581
638
  if (maxDepth <= 0) return;
582
639
 
583
- // Check for SKILL.md in this directory
584
640
  const skillMd = join(dir, "SKILL.md");
585
641
  if (existsSync(skillMd)) {
586
642
  try {
@@ -610,7 +666,7 @@ export class ProfileManager {
610
666
  // 1. Global Pi skills
611
667
  scanDir(join(this.piAgentDir, "skills"), 2, "global");
612
668
 
613
- // 2. Extension skills from npm packages (nested: node_modules/<ext>/skills/<name>/SKILL.md)
669
+ // 2. Extension skills from npm packages
614
670
  const npmDir = join(this.piAgentDir, "npm", "node_modules");
615
671
  if (existsSync(npmDir)) {
616
672
  for (const ext of readdirSync(npmDir)) {
@@ -620,35 +676,12 @@ export class ProfileManager {
620
676
  if (statSync(extSkills).isDirectory()) {
621
677
  scanDir(extSkills, 3, "extension");
622
678
  }
623
- } catch {
624
- // Skip
625
- }
626
- }
627
- }
628
- }
629
-
630
- // 3. Agent-specific skills (nested: agents/<agent>/skills/<name>/SKILL.md)
631
- const agentsDir = join(homedir(), ".pi", "agents");
632
- if (existsSync(agentsDir)) {
633
- try {
634
- for (const agent of readdirSync(agentsDir)) {
635
- const agentSkills = join(agentsDir, agent, "skills");
636
- if (existsSync(agentSkills)) {
637
- try {
638
- if (statSync(agentSkills).isDirectory()) {
639
- scanDir(agentSkills, 3, "agent");
640
- }
641
- } catch {
642
- // Skip
643
- }
644
- }
679
+ } catch {}
645
680
  }
646
- } catch {
647
- // Skip unreadable agents dir
648
681
  }
649
682
  }
650
683
 
651
- // 4. Project-level skills (flat)
684
+ // 3. Project-level skills
652
685
  if (this.cwd) {
653
686
  const projSkillsDir = join(this.cwd, ".pi", "skills");
654
687
  if (existsSync(projSkillsDir)) {