pi-agents-switch 0.2.7 → 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.
package/index.ts CHANGED
@@ -888,23 +888,16 @@ export default function agentsSwitch(pi: ExtensionAPI) {
888
888
  const skillsOpen = "\n<available_skills>";
889
889
  const skillsIdx = content.indexOf(skillsOpen);
890
890
  if (skillsIdx >= 0) {
891
- const skillsEnd = content.indexOf(
892
- "\n</available_skills>",
893
- skillsIdx,
894
- );
891
+ const skillsEnd = content.indexOf("\n</available_skills>", skillsIdx);
895
892
  if (skillsEnd >= 0) {
896
893
  content =
897
894
  content.substring(0, skillsIdx) +
898
- content.substring(
899
- skillsEnd + "\n</available_skills>".length,
900
- );
895
+ content.substring(skillsEnd + "\n</available_skills>".length);
901
896
  }
902
897
  }
903
898
 
904
899
  // Rebuild clean skills block from our resolved config
905
- const selectedSkills = pm.getSkillObjects(
906
- currentResolved.skillNames,
907
- );
900
+ const selectedSkills = pm.getSkillObjects(currentResolved.skillNames);
908
901
  if (selectedSkills.length > 0) {
909
902
  const skillsBlock = formatSkillsBlock(selectedSkills);
910
903
  // Insert before "Current date:"
@@ -912,9 +905,7 @@ export default function agentsSwitch(pi: ExtensionAPI) {
912
905
  const idx = content.indexOf(currentDateMarker);
913
906
  if (idx >= 0) {
914
907
  content =
915
- content.substring(0, idx) +
916
- skillsBlock +
917
- content.substring(idx);
908
+ content.substring(0, idx) + skillsBlock + content.substring(idx);
918
909
  }
919
910
  }
920
911
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agents-switch",
3
- "version": "0.2.7",
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 ───────────────────────