pi-agents-switch 0.3.1 → 0.3.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,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 |
@@ -53,8 +57,7 @@ The Builder will design the agent, write the AGENTS.md, and set everything up.
53
57
  ```
54
58
  ~/.pi/agents/<name>/
55
59
  ├── AGENTS.md ← YAML frontmatter + system prompt (all-in-one)
56
- ├── skills/ ← agent-specific skills (auto-loaded)
57
- └── prompts/ ← agent-specific prompt templates (optional)
60
+ └── skills/ ← agent-specific skills (auto-loaded, optional)
58
61
  ```
59
62
 
60
63
  When you switch to an agent, its `AGENTS.md` body is injected into the system prompt. You can also configure model, thinking level, and tools per agent via the frontmatter.
package/frontmatter.ts CHANGED
@@ -145,7 +145,6 @@ function parseScalar(value: string): string | boolean | number {
145
145
 
146
146
  /**
147
147
  * Serialize frontmatter back to a YAML block (for creating agents).
148
- * Uses the preferred key names: `excluded_tools`, `excluded_extensions`, `excluded_skills`.
149
148
  */
150
149
  export function serializeFrontmatter(fm: AgentFrontmatter): string {
151
150
  const lines: string[] = ["---"];
package/index.ts CHANGED
@@ -140,9 +140,7 @@ If you want to exclude just a few items, add \`*\` to the include list and use t
140
140
 
141
141
  \`\`\`
142
142
  ~/.pi/agents/<name>/
143
- ├── AGENTS.md YAML frontmatter + system prompt (THE ONLY REQUIRED FILE)
144
- ├── skills/ ← Agent-specific skills (mkdir)
145
- └── prompts/ ← Agent-specific prompt templates (mkdir)
143
+ └── skills/ Agent-specific skills (mkdir, optional)
146
144
  \`\`\`
147
145
 
148
146
  ## Workflow: How to create an agent
@@ -752,6 +750,12 @@ export default function agentsSwitch(pi: ExtensionAPI) {
752
750
  section +
753
751
  newPrompt.substring(roleEnd);
754
752
  } else {
753
+ // Pi version mismatch — role line changed. Fall back to prepend.
754
+ console.warn(
755
+ "[agents-switch] Default role line not found in system prompt. " +
756
+ "Pi may have changed its prompt format. " +
757
+ "Agent identity will be prepended instead of replacing the role line.",
758
+ );
755
759
  newPrompt = section + "\n\n" + newPrompt;
756
760
  }
757
761
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agents-switch",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
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": {
@@ -8,8 +8,7 @@
8
8
  * Profile dir layout:
9
9
  * ~/.pi/agents/<name>/ ← user-level agent
10
10
  * ├── AGENTS.md ← YAML frontmatter + system prompt
11
- * ├── skills/ ← agent-specific skills
12
- * └── prompts/ ← agent-specific prompt templates
11
+ * └── skills/ ← agent-specific skills
13
12
  *
14
13
  * <cwd>/.pi/agents/<name>/ ← project-level agent (overrides user)
15
14
  * └── AGENTS.md
@@ -478,12 +477,10 @@ export class ProfileManager {
478
477
  * Look up full skill objects by name from Pi's standard skill directories.
479
478
  * Returns name, description, and filePath for each skill found.
480
479
  */
481
- /** Get PI skill names (global + extension + project, excluding agent-scoped). */
480
+ /** Get all indexed skill names (global + extension + project). */
482
481
  getPISkillNames(): string[] {
483
482
  if (!this.skillCache) this.rebuildSkillIndex();
484
- return [...this.skillCache!.entries()]
485
- .filter(([, v]) => v.source !== "agent")
486
- .map(([k]) => k);
483
+ return [...this.skillCache!.keys()];
487
484
  }
488
485
 
489
486
  /**
@@ -562,17 +559,76 @@ export class ProfileManager {
562
559
  if (!this.skillCache) {
563
560
  this.rebuildSkillIndex();
564
561
  }
565
- return this.skillCache!.get(name);
562
+ const cached = this.skillCache!.get(name);
563
+ if (cached) return cached;
564
+
565
+ // Fallback: agent-specific skills are not indexed globally.
566
+ // Check each agent's skills/ dir on demand.
567
+ return this.findAgentSkillOnDisk(name);
568
+ }
569
+
570
+ /**
571
+ * Look up a single agent-specific skill by checking each agent's
572
+ * skills/<name>/SKILL.md path directly.
573
+ */
574
+ private findAgentSkillOnDisk(
575
+ name: string,
576
+ ): { name: string; description: string; filePath: string } | undefined {
577
+ const read = (
578
+ filePath: string,
579
+ ): { name: string; description: string; filePath: string } | undefined => {
580
+ if (!existsSync(filePath)) return undefined;
581
+ try {
582
+ const content = readFileSync(filePath, "utf8");
583
+ const { frontmatter } = parseFrontmatter(content);
584
+ return {
585
+ name: frontmatter.name ?? name,
586
+ description: frontmatter.description ?? "",
587
+ filePath,
588
+ };
589
+ } catch {
590
+ return undefined;
591
+ }
592
+ };
593
+
594
+ // User-level agents
595
+ const userDir = join(homedir(), ".pi", "agents");
596
+ if (existsSync(userDir)) {
597
+ try {
598
+ for (const agent of readdirSync(userDir)) {
599
+ const found = read(join(userDir, agent, "skills", name, "SKILL.md"));
600
+ if (found) return found;
601
+ }
602
+ } catch {}
603
+ }
604
+
605
+ // Project-level agents
606
+ if (this.cwd) {
607
+ const projDir = join(this.cwd, ".pi", "agents");
608
+ if (existsSync(projDir)) {
609
+ try {
610
+ for (const agent of readdirSync(projDir)) {
611
+ const found = read(
612
+ join(projDir, agent, "skills", name, "SKILL.md"),
613
+ );
614
+ if (found) return found;
615
+ }
616
+ } catch {}
617
+ }
618
+ }
619
+
620
+ return undefined;
566
621
  }
567
622
 
568
623
  /**
569
- * Rebuild the skill index by scanning all known skill directories.
624
+ * Rebuild the skill index by scanning known skill directories.
625
+ * Agent-specific skills are NOT indexed here — they are looked up
626
+ * on demand via findSkillOnDisk → scanForAgentSkill().
570
627
  *
571
628
  * Locations scanned:
572
629
  * 1. ~/.pi/agent/skills/<name>/SKILL.md (flat, user-installed)
573
630
  * 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)
631
+ * 3. <cwd>/.pi/skills/ (project skills, flat)
576
632
  */
577
633
  rebuildSkillIndex(): void {
578
634
  this.skillCache = new Map();
@@ -580,7 +636,6 @@ export class ProfileManager {
580
636
  const scanDir = (dir: string, maxDepth: number, source: string): void => {
581
637
  if (maxDepth <= 0) return;
582
638
 
583
- // Check for SKILL.md in this directory
584
639
  const skillMd = join(dir, "SKILL.md");
585
640
  if (existsSync(skillMd)) {
586
641
  try {
@@ -610,7 +665,7 @@ export class ProfileManager {
610
665
  // 1. Global Pi skills
611
666
  scanDir(join(this.piAgentDir, "skills"), 2, "global");
612
667
 
613
- // 2. Extension skills from npm packages (nested: node_modules/<ext>/skills/<name>/SKILL.md)
668
+ // 2. Extension skills from npm packages
614
669
  const npmDir = join(this.piAgentDir, "npm", "node_modules");
615
670
  if (existsSync(npmDir)) {
616
671
  for (const ext of readdirSync(npmDir)) {
@@ -620,35 +675,12 @@ export class ProfileManager {
620
675
  if (statSync(extSkills).isDirectory()) {
621
676
  scanDir(extSkills, 3, "extension");
622
677
  }
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
- }
678
+ } catch {}
645
679
  }
646
- } catch {
647
- // Skip unreadable agents dir
648
680
  }
649
681
  }
650
682
 
651
- // 4. Project-level skills (flat)
683
+ // 3. Project-level skills
652
684
  if (this.cwd) {
653
685
  const projSkillsDir = join(this.cwd, ".pi", "skills");
654
686
  if (existsSync(projSkillsDir)) {
@@ -685,7 +717,6 @@ export class ProfileManager {
685
717
  try {
686
718
  mkdirSync(path, { recursive: true });
687
719
  mkdirSync(join(path, "skills"));
688
- mkdirSync(join(path, "prompts"));
689
720
  } catch (err) {
690
721
  try {
691
722
  rmSync(path, { recursive: true, force: true });
package/types.ts CHANGED
@@ -62,8 +62,8 @@ export interface FrontmatterResult {
62
62
  body: string;
63
63
  /**
64
64
  * Ordered list of top-level YAML keys as they appear in the file.
65
- * Used for last-write-wins conflict resolution between `tools`/`excluded_tools`,
66
- * `extensions`/`excluded_extensions`/`noextensions`, etc.
65
+ * Used for last-write-wins conflict resolution between `tools`/`excluded_tools`
66
+ * and `skills`/`excluded_skills`.
67
67
  */
68
68
  keyOrder: string[];
69
69
  }