pi-agents-switch 0.2.6 → 0.2.8

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.
Files changed (3) hide show
  1. package/index.ts +16 -22
  2. package/package.json +1 -1
  3. package/profile-manager.ts +105 -15
package/index.ts CHANGED
@@ -881,27 +881,23 @@ export default function agentsSwitch(pi: ExtensionAPI) {
881
881
  }
882
882
  }
883
883
 
884
- // Replace <available_skills> block: strip whatever is there
885
- // (other extensions may inject extra skills), then rebuild
886
- // from our resolved skill set.
887
- const skillsOpen = "\n<available_skills>";
888
- const skillsIdx = content.indexOf(skillsOpen);
889
- if (skillsIdx >= 0) {
890
- const skillsEnd = content.indexOf("\n</available_skills>", skillsIdx);
891
- if (skillsEnd >= 0) {
892
- content =
893
- content.substring(0, skillsIdx) +
894
- content.substring(
895
- skillsEnd + "\n</available_skills>".length,
896
- );
884
+ // Replace <available_skills> block: only touch skills when the agent
885
+ // has explicit skill config (skillNames.length > 0).
886
+ // For agents without skill config, leave Pi's default skills intact.
887
+ if (currentResolved.skillNames.length > 0) {
888
+ const skillsOpen = "\n<available_skills>";
889
+ const skillsIdx = content.indexOf(skillsOpen);
890
+ if (skillsIdx >= 0) {
891
+ const skillsEnd = content.indexOf("\n</available_skills>", skillsIdx);
892
+ if (skillsEnd >= 0) {
893
+ content =
894
+ content.substring(0, skillsIdx) +
895
+ content.substring(skillsEnd + "\n</available_skills>".length);
896
+ }
897
897
  }
898
- }
899
898
 
900
- // Rebuild clean skills block from our resolved config
901
- if (currentResolved.skillNames.length > 0) {
902
- const selectedSkills = pm.getSkillObjects(
903
- currentResolved.skillNames,
904
- );
899
+ // Rebuild clean skills block from our resolved config
900
+ const selectedSkills = pm.getSkillObjects(currentResolved.skillNames);
905
901
  if (selectedSkills.length > 0) {
906
902
  const skillsBlock = formatSkillsBlock(selectedSkills);
907
903
  // Insert before "Current date:"
@@ -909,9 +905,7 @@ export default function agentsSwitch(pi: ExtensionAPI) {
909
905
  const idx = content.indexOf(currentDateMarker);
910
906
  if (idx >= 0) {
911
907
  content =
912
- content.substring(0, idx) +
913
- skillsBlock +
914
- content.substring(idx);
908
+ content.substring(0, idx) + skillsBlock + content.substring(idx);
915
909
  }
916
910
  }
917
911
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agents-switch",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
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": {
@@ -35,7 +35,7 @@ import {
35
35
  statSync,
36
36
  rmSync,
37
37
  } from "node:fs";
38
- import { join } from "node:path";
38
+ import { join, basename } from "node:path";
39
39
  import { homedir } from "node:os";
40
40
  import { parseFrontmatter, serializeFrontmatter } from "./frontmatter";
41
41
  import type {
@@ -65,6 +65,9 @@ export class ProfileManager {
65
65
  readonly configPath: string;
66
66
  readonly piAgentDir: string;
67
67
 
68
+ /** Cached skill index keyed by name (rebuilt on cwd change) */
69
+ private skillCache: Map<string, { name: string; description: string; filePath: string }> | null = null;
70
+
68
71
  constructor(private cwd?: string) {
69
72
  const piRoot = join(homedir(), ".pi");
70
73
  this.userAgentsDir = join(piRoot, "agents");
@@ -460,6 +463,10 @@ export class ProfileManager {
460
463
  * Look up full skill objects by name from Pi's standard skill directories.
461
464
  * Returns name, description, and filePath for each skill found.
462
465
  */
466
+ /**
467
+ * Look up full skill objects by name.
468
+ * Scans Pi's standard skill locations recursively (flat + nested).
469
+ */
463
470
  getSkillObjects(
464
471
  skillNames: string[],
465
472
  ): Array<{ name: string; description: string; filePath: string }> {
@@ -480,28 +487,111 @@ export class ProfileManager {
480
487
  private findSkillOnDisk(
481
488
  name: string,
482
489
  ): { name: string; description: string; filePath: string } | undefined {
483
- const locations = [join(this.piAgentDir, "skills", name, "SKILL.md")];
490
+ if (!this.skillCache) {
491
+ this.rebuildSkillIndex();
492
+ }
493
+ return this.skillCache!.get(name);
494
+ }
484
495
 
485
- if (this.cwd) {
486
- locations.push(join(this.cwd, ".pi", "skills", name, "SKILL.md"));
496
+ /**
497
+ * Rebuild the skill index by scanning all known skill directories.
498
+ *
499
+ * Locations scanned:
500
+ * 1. ~/.pi/agent/skills/<name>/SKILL.md (flat, user-installed)
501
+ * 2. ~/.pi/agent/npm/node_modules/<ext>/skills/ (extension skills)
502
+ * 3. ~/.pi/agents/<agent>/skills/ (agent-specific skills)
503
+ * 4. <cwd>/.pi/skills/ (project skills, flat)
504
+ */
505
+ rebuildSkillIndex(): void {
506
+ this.skillCache = new Map();
507
+
508
+ const scanDir = (dir: string, maxDepth: number): void => {
509
+ if (maxDepth <= 0) return;
510
+
511
+ // Check for SKILL.md in this directory
512
+ const skillMd = join(dir, "SKILL.md");
513
+ if (existsSync(skillMd)) {
514
+ try {
515
+ const content = readFileSync(skillMd, "utf8");
516
+ const { frontmatter } = parseFrontmatter(content);
517
+ const skillName = frontmatter.name ?? basename(dir);
518
+ // Last-write-wins: later scan locations override earlier ones
519
+ this.skillCache!.set(skillName, {
520
+ name: skillName,
521
+ description: frontmatter.description ?? "",
522
+ filePath: skillMd,
523
+ });
524
+ } catch {
525
+ // Skip unreadable files
526
+ }
527
+ }
528
+
529
+ // Recurse into subdirectories
530
+ try {
531
+ for (const entry of readdirSync(dir)) {
532
+ if (entry === "node_modules") continue; // skip nested node_modules
533
+ const sub = join(dir, entry);
534
+ try {
535
+ if (statSync(sub).isDirectory()) {
536
+ scanDir(sub, maxDepth - 1);
537
+ }
538
+ } catch {
539
+ // Skip unreadable entries
540
+ }
541
+ }
542
+ } catch {
543
+ // Skip unreadable directories
544
+ }
545
+ };
546
+
547
+ // 1. Global Pi skills (flat: skills/<name>/SKILL.md)
548
+ scanDir(join(this.piAgentDir, "skills"), 2);
549
+
550
+ // 2. Extension skills from npm packages (nested: node_modules/<ext>/skills/<name>/SKILL.md)
551
+ const npmDir = join(this.piAgentDir, "npm", "node_modules");
552
+ if (existsSync(npmDir)) {
553
+ for (const ext of readdirSync(npmDir)) {
554
+ const extSkills = join(npmDir, ext, "skills");
555
+ if (existsSync(extSkills)) {
556
+ try {
557
+ if (statSync(extSkills).isDirectory()) {
558
+ scanDir(extSkills, 3);
559
+ }
560
+ } catch {
561
+ // Skip
562
+ }
563
+ }
564
+ }
487
565
  }
488
566
 
489
- for (const loc of locations) {
490
- if (!existsSync(loc)) continue;
567
+ // 3. Agent-specific skills (nested: agents/<agent>/skills/<name>/SKILL.md)
568
+ const agentsDir = join(homedir(), ".pi", "agents");
569
+ if (existsSync(agentsDir)) {
491
570
  try {
492
- const content = readFileSync(loc, "utf8");
493
- const { frontmatter } = parseFrontmatter(content);
494
- return {
495
- name: frontmatter.name ?? name,
496
- description: frontmatter.description ?? "",
497
- filePath: loc,
498
- };
571
+ for (const agent of readdirSync(agentsDir)) {
572
+ const agentSkills = join(agentsDir, agent, "skills");
573
+ if (existsSync(agentSkills)) {
574
+ try {
575
+ if (statSync(agentSkills).isDirectory()) {
576
+ scanDir(agentSkills, 3);
577
+ }
578
+ } catch {
579
+ // Skip
580
+ }
581
+ }
582
+ }
499
583
  } catch {
500
- // Skip unreadable files
584
+ // Skip unreadable agents dir
501
585
  }
502
586
  }
503
587
 
504
- return undefined;
588
+ // 4. Project-level skills (flat)
589
+ if (this.cwd) {
590
+ const projSkillsDir = join(this.cwd, ".pi", "skills");
591
+ if (existsSync(projSkillsDir)) {
592
+ scanDir(projSkillsDir, 2);
593
+ }
594
+ }
505
595
  }
506
596
 
507
597
  // ─── Agent creation / deletion ───────────────────────