pi-agents-switch 0.2.4 → 0.2.5

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/frontmatter.ts CHANGED
@@ -171,7 +171,6 @@ export function serializeFrontmatter(fm: AgentFrontmatter): string {
171
171
  writeArray("tools", fm.tools);
172
172
  writeArray("excluded_tools", fm.excluded_tools);
173
173
 
174
-
175
174
  writeArray("skills", fm.skills);
176
175
  writeArray("excluded_skills", fm.excluded_skills ?? fm.noskills);
177
176
 
package/index.ts CHANGED
@@ -769,9 +769,8 @@ export default function agentsSwitch(pi: ExtensionAPI) {
769
769
 
770
770
  // Append skills section if agent has resolved skills
771
771
  if (currentResolved && currentResolved.skillNames.length > 0) {
772
- const loadedSkills = (
773
- event.systemPromptOptions as { skills?: any[] }
774
- ).skills;
772
+ const loadedSkills = (event.systemPromptOptions as { skills?: any[] })
773
+ .skills;
775
774
  let selectedSkills: Array<{
776
775
  name: string;
777
776
  description: string;
@@ -802,12 +801,14 @@ export default function agentsSwitch(pi: ExtensionAPI) {
802
801
  return { systemPrompt: newPrompt };
803
802
  });
804
803
 
805
- // ─── before_provider_request ────────────────────────
804
+ // ─── before_provider_request: enforce tool set + sanitize ─
806
805
 
807
806
  pi.on("before_provider_request", (event) => {
808
- if (!currentResolved) return;
807
+ if (!currentResolved || currentAgent === PI_AGENT_NAME) return;
809
808
  const payload = event.payload as Record<string, unknown>;
810
809
  let changed = false;
810
+
811
+ // Temperature / topP
811
812
  if (currentResolved.temperature !== undefined) {
812
813
  payload.temperature = currentResolved.temperature;
813
814
  changed = true;
@@ -816,6 +817,111 @@ export default function agentsSwitch(pi: ExtensionAPI) {
816
817
  payload.top_p = currentResolved.topP;
817
818
  changed = true;
818
819
  }
820
+
821
+ // ---- Sanitization: prevent other extensions from contaminating ----
822
+ // Other extensions (e.g. pi-lens) may override our tools and prompt
823
+ // in before_agent_start (last-runner-wins). We re-apply our config
824
+ // here as the final defence before the API call.
825
+
826
+ // 1. Filter tools — only allow resolved tools
827
+ const allowedTools = new Set(currentResolved.tools);
828
+ const rawTools = payload.tools as
829
+ | Array<{ function?: { name?: string } }>
830
+ | undefined;
831
+ if (rawTools && rawTools.length > 0) {
832
+ const before = rawTools.length;
833
+ payload.tools = rawTools.filter((t) =>
834
+ allowedTools.has(t?.function?.name ?? ""),
835
+ );
836
+ if ((payload.tools as Array<any>).length !== before) changed = true;
837
+ }
838
+
839
+ // 2. Clean system message — replace contaminated sections
840
+ const messages = payload.messages as
841
+ | Array<{ role: string; content: string }>
842
+ | undefined;
843
+ if (messages && messages.length > 0 && messages[0]?.role === "system") {
844
+ let content: string = messages[0].content;
845
+ const originalLen = content.length;
846
+
847
+ // Strip the "Available tools:" block (other extensions may inject their own)
848
+ // Format: "Available tools:\n- name: desc\n..."
849
+ // We replace it with our clean version.
850
+ const toolsMarker = "\nAvailable tools:";
851
+ const toolsIdx = content.indexOf(toolsMarker);
852
+ if (toolsIdx >= 0) {
853
+ // Find where the tools block ends (next section or empty line before next section)
854
+ const nextSection = content.indexOf(
855
+ "\nIn addition to the tools above",
856
+ toolsIdx,
857
+ );
858
+ if (nextSection >= 0) {
859
+ // Build clean tools list
860
+ if (currentResolved.tools.length === 0) {
861
+ content =
862
+ content.substring(0, toolsIdx) +
863
+ "\nAvailable tools:\n(none)\n" +
864
+ content.substring(nextSection);
865
+ } else {
866
+ // Rebuild clean tools from tool snippets if available
867
+ const snippets = pi.getAllTools();
868
+ const cleanLines = currentResolved.tools
869
+ .map((name) => {
870
+ const tool = snippets.find(
871
+ (t) => t.name === name,
872
+ );
873
+ return tool
874
+ ? `- ${tool.name}: ${tool.description ?? ""}`
875
+ : `- ${name}`;
876
+ })
877
+ .join("\n");
878
+ content =
879
+ content.substring(0, toolsIdx) +
880
+ `\nAvailable tools:\n${cleanLines}\n` +
881
+ content.substring(nextSection);
882
+ }
883
+ }
884
+ }
885
+
886
+ // Strip the <available_skills> block (other extensions might add extra skills)
887
+ // Our before_agent_start handler manages this — any extras are contamination.
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(
896
+ skillsEnd + "\n</available_skills>".length,
897
+ );
898
+ }
899
+ }
900
+
901
+ // Strip "Pi documentation" block (pi-lens may re-add after our handler strips it)
902
+ const piDocsMarker =
903
+ "Pi documentation (read only when the user asks about pi itself";
904
+ const piDocsStart = content.indexOf(piDocsMarker);
905
+ if (piDocsStart >= 0) {
906
+ const currentDateMarker = "\nCurrent date:";
907
+ const currentDateStart = content.indexOf(
908
+ currentDateMarker,
909
+ piDocsStart,
910
+ );
911
+ if (currentDateStart >= 0) {
912
+ content =
913
+ content.substring(0, piDocsStart) +
914
+ content.substring(currentDateStart);
915
+ }
916
+ }
917
+
918
+ if (content.length !== originalLen) {
919
+ messages[0].content = content;
920
+ changed = true;
921
+ }
922
+ }
923
+
924
+ // Return payload only if we changed something (otherwise Pi uses default)
819
925
  if (changed) return payload;
820
926
  });
821
927
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agents-switch",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
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": {
@@ -480,9 +480,7 @@ export class ProfileManager {
480
480
  private findSkillOnDisk(
481
481
  name: string,
482
482
  ): { name: string; description: string; filePath: string } | undefined {
483
- const locations = [
484
- join(this.piAgentDir, "skills", name, "SKILL.md"),
485
- ];
483
+ const locations = [join(this.piAgentDir, "skills", name, "SKILL.md")];
486
484
 
487
485
  if (this.cwd) {
488
486
  locations.push(join(this.cwd, ".pi", "skills", name, "SKILL.md"));