pi-agents-switch 0.2.2 → 0.2.4

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
@@ -53,7 +53,6 @@ The Builder will design the agent, write the AGENTS.md, and set everything up.
53
53
  ```
54
54
  ~/.pi/agents/<name>/
55
55
  ├── AGENTS.md ← YAML frontmatter + system prompt (all-in-one)
56
- ├── extensions/ ← agent-specific extensions (optional)
57
56
  ├── skills/ ← agent-specific skills (optional)
58
57
  └── prompts/ ← agent-specific prompt templates (optional)
59
58
  ```
@@ -80,14 +79,10 @@ excluded_tools:
80
79
  - edit
81
80
  - write
82
81
 
83
- # Extensions / skills (same inheritance pattern)
84
- extensions:
85
- - my-custom-ext
86
- noextensions:
87
- - annoying-ext
82
+ # Skills (same inheritance pattern)
88
83
  skills:
89
84
  - my-skill
90
- noskills:
85
+ excluded_skills:
91
86
  - noisy-skill
92
87
  ---
93
88
 
@@ -101,11 +96,11 @@ Your role and specific instructions go here.
101
96
 
102
97
  Each agent starts with PI's current config, then applies:
103
98
 
104
- 1. **Remove** items listed in `excluded_tools` / `noextensions` / `noskills`
105
- 2. **Add** items listed in `tools` / `extensions` / `skills`
99
+ 1. **Remove** items listed in `excluded_tools` / `excluded_skills`
100
+ 2. **Add** items listed in `tools` / `skills`
106
101
  3. If an item appears in **both** `tools` and `excluded_tools`, `tools` wins (explicit include beats exclude)
107
102
 
108
- Agent-specific folders (`extensions/`, `skills/`, `prompts/`) are always included in addition to inherited items.
103
+ Agent-specific folders (`skills/`, `prompts/`) are always included in addition to inherited items.
109
104
 
110
105
  ### Fallback Chain
111
106
 
@@ -113,7 +108,7 @@ When resolving an agent's config:
113
108
 
114
109
  1. **Agent-level**: `~/.pi/agents/<name>/AGENTS.md`
115
110
  2. **Project-level**: `<cwd>/.pi/agents/<name>/AGENTS.md` — overrides agent-level fields
116
- 3. **PI defaults**: tools/extensions/skills from your running PI config
111
+ 3. **PI defaults**: tools/skills from your running PI config
117
112
 
118
113
  ## Creating agents
119
114
 
@@ -172,10 +167,8 @@ Agents are auto-discovered from the filesystem — no registration needed.
172
167
  | `topP` | number | Nucleus sampling threshold (0-1) |
173
168
  | `tools` | string[] | Tools to ADD after inheritance |
174
169
  | `excluded_tools` | string[] | Tools to REMOVE from PI's inherited set |
175
- | `extensions` | string[] | Extensions to ADD |
176
- | `noextensions` | string[] | Extensions to REMOVE |
177
170
  | `skills` | string[] | Skills to ADD |
178
- | `noskills` | string[] | Skills to REMOVE |
171
+ | `excluded_skills` | string[] | Skills to REMOVE |
179
172
 
180
173
  The Markdown body (everything after the last `---`) is the **system prompt** injected before each agent turn.
181
174
 
package/frontmatter.ts CHANGED
@@ -170,8 +170,8 @@ export function serializeFrontmatter(fm: AgentFrontmatter): string {
170
170
 
171
171
  writeArray("tools", fm.tools);
172
172
  writeArray("excluded_tools", fm.excluded_tools);
173
- writeArray("extensions", fm.extensions);
174
- writeArray("excluded_extensions", fm.excluded_extensions ?? fm.noextensions);
173
+
174
+
175
175
  writeArray("skills", fm.skills);
176
176
  writeArray("excluded_skills", fm.excluded_skills ?? fm.noskills);
177
177
 
package/index.ts CHANGED
@@ -109,12 +109,6 @@ excluded_tools: # Tools to REMOVE from PI's default set
109
109
  tools: # Tools to ADD
110
110
  -what you want
111
111
 
112
- excluded_extensions: # Extensions to REMOVE
113
- - annoying-ext
114
-
115
- extensions: # Extensions to ADD
116
- - my-extension
117
-
118
112
  excluded_skills: # Skills to REMOVE
119
113
  - noisy-skill
120
114
 
@@ -132,14 +126,14 @@ Your behavior instructions...
132
126
 
133
127
  Every agent starts with what PI already has, then:
134
128
 
135
- 1. **Remove** items in \`excluded_tools\` / \`excluded_extensions\` / \`excluded_skills\`
136
- 2. **Add** items in \`tools\` / \`extensions\` / \`skills\`
129
+ 1. **Remove** items in \`excluded_tools\` / \`excluded_skills\`
130
+ 2. **Add** items in \`tools\` / \`skills\`
137
131
  3. - **Order matters.** If the same item appears in both an exclude field and an add field, the later declaration wins.
138
132
  - \`excluded_tools: [read]\` then \`tools: [read]\` → \`read\` is added.
139
133
  - \`tools: [read]\` then \`excluded_tools: [read]\` → \`read\` is removed.
140
134
  - \`*\` means "all" for that category.
141
135
 
142
- If you want to keep only a small set of tools/skills/extensions, exclude everything with \`*\` and add back what you need.
136
+ If you want to keep only a small set of tools/skills, exclude everything with \`*\` and add back what you need.
143
137
  If you want to exclude just a few items, add \`*\` to the include list and use the exclude to remove the unwanted ones.
144
138
 
145
139
  ### Agent Directory Structure
@@ -147,7 +141,6 @@ If you want to exclude just a few items, add \`*\` to the include list and use t
147
141
  \`\`\`
148
142
  ~/.pi/agents/<name>/
149
143
  ├── AGENTS.md ← YAML frontmatter + system prompt (THE ONLY REQUIRED FILE)
150
- ├── extensions/ ← Agent-specific extensions (mkdir)
151
144
  ├── skills/ ← Agent-specific skills (mkdir)
152
145
  └── prompts/ ← Agent-specific prompt templates (mkdir)
153
146
  \`\`\`
@@ -166,7 +159,7 @@ Inventory what's available so you can make grounded recommendations:
166
159
  4. **Skills currently available**: check \`<available_skills>\` block in your context, or \`ls ~/.pi/agent/skills/\` and \`ls ~/.agents/skills/\`
167
160
  5. **Extensions installed**: \`ls ~/.pi/agent/npm/node_modules/\` for pi-* packages
168
161
 
169
- **Never** recommend models, tools, skills, or extensions the user doesn't have installed.
162
+ **Never** recommend models, tools, or skills the user doesn't have installed.
170
163
 
171
164
  ### Phase 1 — Gather requirements (single \`ask_user_question\` call, 4-6 questions)
172
165
 
@@ -181,7 +174,7 @@ Cover every parameter the agent needs. Skip this phase only if the user's reques
181
174
  | 3 | **Model** | Provider + model (from Phase 0 catalog); \`thinkingLevel\`: off / minimal / low / medium / high / xhigh |
182
175
  | 4 | **Tools** | Which to add/remove? Start from a preset, let user customize |
183
176
  | 5 | **Skills** | Which to add (\`skills\`) or remove (\`excluded_skills\`)? Reference Phase 0 catalog |
184
- | 6 | **Extensions** | Which to add (\`extensions\`) or remove (\`excluded_extensions\`)? Reference Phase 0 catalog |
177
+
185
178
 
186
179
  **Tool presets** (offer as starting points):
187
180
 
@@ -192,7 +185,7 @@ Cover every parameter the agent needs. Skip this phase only if the user's reques
192
185
  | 🛠️ Full-access | (inherit all PI defaults) | — | Building, refactoring, full-stack work |
193
186
  | 📦 Minimal | \`[bash, read, write]\` | everything else | Shell scripting, simple file ops |
194
187
 
195
- If user picks a preset, still confirm skills/extensions/model choices.
188
+ If user picks a preset, still confirm skills/model choices.
196
189
 
197
190
  ### Phase 2 — Build
198
191
 
@@ -204,11 +197,10 @@ If user picks a preset, still confirm skills/extensions/model choices.
204
197
  ### Phase 3 — Verify & deliver
205
198
 
206
199
  - Confirm file exists; spot-check YAML frontmatter
207
- - Report: display name, switch command (\`/switch <name>\`), provider/model, tool preset, skills/extensions delta
200
+ - Report: display name, switch command (\`/switch <name>\`), provider/model, tool preset, skills delta
208
201
  - Offer: "Switch now with \`/switch <name>\` to test."
209
202
  `;
210
203
 
211
-
212
204
  // ─── PI state snapshot ─────────────────────────────────
213
205
 
214
206
  interface PIStateSnapshot {
@@ -269,11 +261,48 @@ export default function agentsSwitch(pi: ExtensionAPI) {
269
261
  function getPIState(): PIState {
270
262
  return {
271
263
  tools: pi.getActiveTools(),
272
- extensions: [],
273
264
  skills: [],
274
265
  };
275
266
  }
276
267
 
268
+ // ─── Skill formatting helpers ────────────────────────
269
+
270
+ function escapeXml(str: string): string {
271
+ return str
272
+ .replace(/&/g, "&amp;")
273
+ .replace(/</g, "&lt;")
274
+ .replace(/>/g, "&gt;")
275
+ .replace(/"/g, "&quot;")
276
+ .replace(/'/g, "&apos;");
277
+ }
278
+
279
+ function formatSkillsBlock(
280
+ skills: Array<{
281
+ name: string;
282
+ description: string;
283
+ filePath: string;
284
+ }>,
285
+ ): string {
286
+ const lines = [
287
+ "\n\nThe following skills provide specialized instructions for specific tasks.",
288
+ "Use the read tool to load a skill's file when the task matches its description.",
289
+ "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
290
+ "",
291
+ "<available_skills>",
292
+ ];
293
+ for (const skill of skills) {
294
+ lines.push(" <skill>");
295
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
296
+ lines.push(
297
+ ` <description>${escapeXml(skill.description)}</description>`,
298
+ );
299
+ lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
300
+ lines.push(" </skill>");
301
+ }
302
+ lines.push("</available_skills>");
303
+ return lines.join("\n");
304
+ }
305
+
277
306
  // ─── State snapshot / restore ───────────────────────
278
307
 
279
308
  function snapshotPIState(): void {
@@ -695,31 +724,82 @@ export default function agentsSwitch(pi: ExtensionAPI) {
695
724
  },
696
725
  });
697
726
 
698
- // ─── before_agent_start ─────────────────────────────
727
+ // ─── before_agent_start: inject agent identity ──
699
728
 
700
729
  pi.on("before_agent_start", async (event, ctx) => {
701
730
  initPM(ctx.cwd);
702
731
  loadCurrentAgent(pm.loadConfig());
703
- await applyAgentSettings(ctx);
704
732
  updateStatus(ctx);
733
+
734
+ if (currentAgent === PI_AGENT_NAME) return;
735
+
705
736
  const section = getAgentSystemPromptSection();
706
- if (!section) return;
707
-
708
- // Replace the default role line with the agent's identity banner.
709
- // Everything else (tools, guidelines, skills, etc.) stays intact.
710
- const roleStart = event.systemPrompt.indexOf(DEFAULT_ROLE_LINE);
711
- if (roleStart >= 0) {
712
- const roleEnd = roleStart + DEFAULT_ROLE_LINE.length;
713
- const newPrompt =
714
- event.systemPrompt.substring(0, roleStart) +
715
- section +
716
- event.systemPrompt.substring(roleEnd);
717
- return { systemPrompt: newPrompt };
737
+ let newPrompt = event.systemPrompt;
738
+
739
+ // Strip Pi documentation section for non-PI agents
740
+ const piDocsMarker =
741
+ "Pi documentation (read only when the user asks about pi itself";
742
+ const piDocsStart = newPrompt.indexOf(piDocsMarker);
743
+ if (piDocsStart >= 0) {
744
+ const currentDateMarker = "\nCurrent date:";
745
+ const currentDateStart = newPrompt.indexOf(
746
+ currentDateMarker,
747
+ piDocsStart,
748
+ );
749
+ if (currentDateStart >= 0) {
750
+ newPrompt =
751
+ newPrompt.substring(0, piDocsStart) +
752
+ newPrompt.substring(currentDateStart);
753
+ }
754
+ }
755
+
756
+ // Replace or prepend agent identity
757
+ if (section) {
758
+ const roleStart = newPrompt.indexOf(DEFAULT_ROLE_LINE);
759
+ if (roleStart >= 0) {
760
+ const roleEnd = roleStart + DEFAULT_ROLE_LINE.length;
761
+ newPrompt =
762
+ newPrompt.substring(0, roleStart) +
763
+ section +
764
+ newPrompt.substring(roleEnd);
765
+ } else {
766
+ newPrompt = section + "\n\n" + newPrompt;
767
+ }
768
+ }
769
+
770
+ // Append skills section if agent has resolved skills
771
+ if (currentResolved && currentResolved.skillNames.length > 0) {
772
+ const loadedSkills = (
773
+ event.systemPromptOptions as { skills?: any[] }
774
+ ).skills;
775
+ let selectedSkills: Array<{
776
+ name: string;
777
+ description: string;
778
+ filePath: string;
779
+ }>;
780
+
781
+ if (loadedSkills && loadedSkills.length > 0) {
782
+ selectedSkills = loadedSkills.filter((s) =>
783
+ currentResolved!.skillNames.includes(s.name),
784
+ );
785
+ } else {
786
+ selectedSkills = pm.getSkillObjects(currentResolved.skillNames);
787
+ }
788
+
789
+ if (selectedSkills.length > 0) {
790
+ const skillsBlock = formatSkillsBlock(selectedSkills);
791
+ const currentDateMarker = "\nCurrent date:";
792
+ const idx = newPrompt.indexOf(currentDateMarker);
793
+ if (idx >= 0) {
794
+ newPrompt =
795
+ newPrompt.substring(0, idx) +
796
+ skillsBlock +
797
+ newPrompt.substring(idx);
798
+ }
799
+ }
718
800
  }
719
801
 
720
- // Fallback: if the role line wasn't found (e.g. custom SYSTEM.md),
721
- // prepend the agent section at the top instead of appending.
722
- return { systemPrompt: section + "\n\n" + event.systemPrompt };
802
+ return { systemPrompt: newPrompt };
723
803
  });
724
804
 
725
805
  // ─── before_provider_request ────────────────────────
@@ -743,7 +823,11 @@ export default function agentsSwitch(pi: ExtensionAPI) {
743
823
 
744
824
  pi.on("session_start", async (_event, ctx) => {
745
825
  initPM(ctx.cwd);
746
- loadCurrentAgent(pm.loadConfig());
826
+ const config = pm.loadConfig();
827
+ loadCurrentAgent(config);
828
+ // Apply agent settings immediately so tools/model/thinking
829
+ // are set before the first prompt builds the system prompt.
830
+ await applyAgentSettings(ctx);
747
831
  if (currentAgent !== PI_AGENT_NAME)
748
832
  currentResolved = pm.resolveAgent(currentAgent, getPIState(), ctx.cwd);
749
833
  updateStatus(ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agents-switch",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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": {
@@ -56,8 +56,6 @@ const MAX_NAME_LENGTH = 32;
56
56
  export interface PIState {
57
57
  /** Active tool names in PI */
58
58
  tools: string[];
59
- /** Active extension names in PI */
60
- extensions: string[];
61
59
  /** Active skill names in PI */
62
60
  skills: string[];
63
61
  }
@@ -341,7 +339,7 @@ export class ProfileManager {
341
339
  agentPath?: string,
342
340
  projectPath?: string,
343
341
  ): ResolvedAgentConfig {
344
- const pi = piState ?? { tools: [], extensions: [], skills: [] };
342
+ const pi = piState ?? { tools: [], skills: [] };
345
343
 
346
344
  // Apply inheritance: start with PI's → respect last-write-wins
347
345
  const tools = this.resolveList(
@@ -353,25 +351,6 @@ export class ProfileManager {
353
351
  keyOrder,
354
352
  );
355
353
 
356
- // Extensions: prefer excluded_extensions, fall back to deprecated noextensions
357
- const extExclude = fm.excluded_extensions ?? fm.noextensions;
358
- const extExcludeKey =
359
- fm.excluded_extensions !== undefined
360
- ? "excluded_extensions"
361
- : fm.noextensions !== undefined
362
- ? "noextensions"
363
- : undefined;
364
- const extensionPaths = extExcludeKey
365
- ? this.resolveList(
366
- pi.extensions,
367
- extExclude,
368
- fm.extensions,
369
- "extensions",
370
- extExcludeKey,
371
- keyOrder,
372
- )
373
- : [...pi.extensions, ...(fm.extensions ?? [])];
374
-
375
354
  // Skills: prefer excluded_skills, fall back to deprecated noskills
376
355
  const skillExclude = fm.excluded_skills ?? fm.noskills;
377
356
  const skillExcludeKey =
@@ -391,22 +370,6 @@ export class ProfileManager {
391
370
  )
392
371
  : [...pi.skills, ...(fm.skills ?? [])];
393
372
 
394
- // Add agent-specific folders
395
- const agentExtensions: string[] = [];
396
- const agentSkills: string[] = [];
397
-
398
- const effectiveAgentDir = projectPath ?? agentPath;
399
- if (effectiveAgentDir) {
400
- const extDir = join(effectiveAgentDir, "extensions");
401
- if (existsSync(extDir)) {
402
- agentExtensions.push(extDir);
403
- }
404
- const skillDir = join(effectiveAgentDir, "skills");
405
- if (existsSync(skillDir)) {
406
- agentSkills.push(skillDir);
407
- }
408
- }
409
-
410
373
  const sourcePath = projectPath ?? agentPath ?? "PI (default)";
411
374
 
412
375
  const modelStr =
@@ -422,8 +385,7 @@ export class ProfileManager {
422
385
  topP: fm.topP,
423
386
  systemPrompt: "", // filled in later by caller
424
387
  tools,
425
- extensionPaths: [...extensionPaths, ...agentExtensions],
426
- skillPaths: [...skillPaths, ...agentSkills],
388
+ skillNames: skillPaths,
427
389
  sourcePath,
428
390
  exists: true,
429
391
  };
@@ -494,6 +456,56 @@ export class ProfileManager {
494
456
  return result;
495
457
  }
496
458
 
459
+ /**
460
+ * Look up full skill objects by name from Pi's standard skill directories.
461
+ * Returns name, description, and filePath for each skill found.
462
+ */
463
+ getSkillObjects(
464
+ skillNames: string[],
465
+ ): Array<{ name: string; description: string; filePath: string }> {
466
+ const result: Array<{
467
+ name: string;
468
+ description: string;
469
+ filePath: string;
470
+ }> = [];
471
+
472
+ for (const name of skillNames) {
473
+ const skill = this.findSkillOnDisk(name);
474
+ if (skill) result.push(skill);
475
+ }
476
+
477
+ return result;
478
+ }
479
+
480
+ private findSkillOnDisk(
481
+ name: string,
482
+ ): { name: string; description: string; filePath: string } | undefined {
483
+ const locations = [
484
+ join(this.piAgentDir, "skills", name, "SKILL.md"),
485
+ ];
486
+
487
+ if (this.cwd) {
488
+ locations.push(join(this.cwd, ".pi", "skills", name, "SKILL.md"));
489
+ }
490
+
491
+ for (const loc of locations) {
492
+ if (!existsSync(loc)) continue;
493
+ try {
494
+ const content = readFileSync(loc, "utf8");
495
+ const { frontmatter } = parseFrontmatter(content);
496
+ return {
497
+ name: frontmatter.name ?? name,
498
+ description: frontmatter.description ?? "",
499
+ filePath: loc,
500
+ };
501
+ } catch {
502
+ // Skip unreadable files
503
+ }
504
+ }
505
+
506
+ return undefined;
507
+ }
508
+
497
509
  // ─── Agent creation / deletion ───────────────────────
498
510
 
499
511
  validateName(name: string): void {
package/types.ts CHANGED
@@ -46,17 +46,6 @@ export interface AgentFrontmatter {
46
46
  */
47
47
  excluded_tools?: string[];
48
48
 
49
- // ── Extension lists ──
50
- /** Extensions to ADD. */
51
- extensions?: string[];
52
- /**
53
- * Extensions to REMOVE (from PI's inherited set). Supports `"*"` wildcard.
54
- * Preferred over deprecated `noextensions`.
55
- */
56
- excluded_extensions?: string[];
57
- /** @deprecated Use `excluded_extensions` instead. Still recognized for backward compat. */
58
- noextensions?: string[];
59
-
60
49
  // ── Skill lists ──
61
50
  /** Skills to ADD. */
62
51
  skills?: string[];
@@ -107,10 +96,8 @@ export interface ResolvedAgentConfig {
107
96
  systemPrompt: string;
108
97
  /** Resolved tools for this agent */
109
98
  tools: string[];
110
- /** Resolved extensions for this agent (from agent-specific folder) */
111
- extensionPaths: string[];
112
- /** Resolved skills for this agent (from agent-specific folder) */
113
- skillPaths: string[];
99
+ /** Resolved skill names for this agent */
100
+ skillNames: string[];
114
101
  /** Source of this agent (path to AGENTS.md used) */
115
102
  sourcePath: string;
116
103
  /** Whether this agent exists on disk */