pi-agents-switch 0.2.8 → 0.3.1
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 +3 -3
- package/frontmatter.ts +1 -1
- package/index.ts +41 -133
- package/package.json +1 -1
- package/profile-manager.ts +112 -50
- package/types.ts +1 -6
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ 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
|
-
├── skills/ ← agent-specific skills (
|
|
56
|
+
├── skills/ ← agent-specific skills (auto-loaded)
|
|
57
57
|
└── prompts/ ← agent-specific prompt templates (optional)
|
|
58
58
|
```
|
|
59
59
|
|
|
@@ -100,7 +100,7 @@ Each agent starts with PI's current config, then applies:
|
|
|
100
100
|
2. **Add** items listed in `tools` / `skills`
|
|
101
101
|
3. If an item appears in **both** `tools` and `excluded_tools`, `tools` wins (explicit include beats exclude)
|
|
102
102
|
|
|
103
|
-
Agent-specific
|
|
103
|
+
Agent-specific skills (`skills/` directory) are auto-discovered and always included.
|
|
104
104
|
|
|
105
105
|
### Fallback Chain
|
|
106
106
|
|
|
@@ -150,7 +150,7 @@ Agents are auto-discovered from the filesystem — no registration needed.
|
|
|
150
150
|
{
|
|
151
151
|
"version": 1,
|
|
152
152
|
"hotkey": "f9",
|
|
153
|
-
"
|
|
153
|
+
"default": "PI"
|
|
154
154
|
}
|
|
155
155
|
```
|
|
156
156
|
|
package/frontmatter.ts
CHANGED
|
@@ -172,7 +172,7 @@ export function serializeFrontmatter(fm: AgentFrontmatter): string {
|
|
|
172
172
|
writeArray("excluded_tools", fm.excluded_tools);
|
|
173
173
|
|
|
174
174
|
writeArray("skills", fm.skills);
|
|
175
|
-
writeArray("excluded_skills", fm.excluded_skills
|
|
175
|
+
writeArray("excluded_skills", fm.excluded_skills);
|
|
176
176
|
|
|
177
177
|
lines.push("---");
|
|
178
178
|
lines.push(""); // blank line before body
|
package/index.ts
CHANGED
|
@@ -157,9 +157,7 @@ Inventory what's available so you can make grounded recommendations:
|
|
|
157
157
|
2. **Existing agents**: \`ls ~/.pi/agents/\` (and project \`.pi/agents/\` if applicable) → avoid name collisions
|
|
158
158
|
3. **Tools currently available**: list names from your system context tool definitions
|
|
159
159
|
4. **Skills currently available**: check \`<available_skills>\` block in your context, or \`ls ~/.pi/agent/skills/\` and \`ls ~/.agents/skills/\`
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
**Never** recommend models, tools, or skills the user doesn't have installed.
|
|
160
|
+
\n**Never** recommend models, tools, or skills the user doesn't have installed.
|
|
163
161
|
|
|
164
162
|
### Phase 1 — Gather requirements (single \`ask_user_question\` call, 4-6 questions)
|
|
165
163
|
|
|
@@ -225,6 +223,7 @@ export default function agentsSwitch(pi: ExtensionAPI) {
|
|
|
225
223
|
let currentAgent: string = PI_AGENT_NAME;
|
|
226
224
|
let currentResolved: ResolvedAgentConfig | undefined;
|
|
227
225
|
let piSnapshot: PIStateSnapshot | undefined;
|
|
226
|
+
let sessionInitialized = false;
|
|
228
227
|
|
|
229
228
|
// ─── Helpers ────────────────────────────────────────
|
|
230
229
|
|
|
@@ -261,7 +260,7 @@ export default function agentsSwitch(pi: ExtensionAPI) {
|
|
|
261
260
|
function getPIState(): PIState {
|
|
262
261
|
return {
|
|
263
262
|
tools: pi.getActiveTools(),
|
|
264
|
-
skills:
|
|
263
|
+
skills: pm.getPISkillNames(),
|
|
265
264
|
};
|
|
266
265
|
}
|
|
267
266
|
|
|
@@ -397,22 +396,16 @@ export default function agentsSwitch(pi: ExtensionAPI) {
|
|
|
397
396
|
// ─── Agent cycling / switching ──────────────────────
|
|
398
397
|
|
|
399
398
|
async function cycleAgent(ctx?: any): Promise<void> {
|
|
400
|
-
const config = pm.loadConfig();
|
|
401
399
|
const names = [PI_AGENT_NAME, ...pm.getAgentNames()];
|
|
402
400
|
const idx = names.indexOf(currentAgent);
|
|
403
401
|
const next = names[(idx + 1) % names.length];
|
|
404
|
-
config.active = next === PI_AGENT_NAME ? PI_AGENT_NAME : next;
|
|
405
|
-
pm.saveConfig(config);
|
|
406
402
|
currentAgent = next;
|
|
407
403
|
await applyAgentSettings(ctx);
|
|
408
404
|
updateStatus(ctx);
|
|
409
405
|
}
|
|
410
406
|
|
|
411
407
|
async function switchToAgent(name: string, ctx?: any): Promise<void> {
|
|
412
|
-
const config = pm.loadConfig();
|
|
413
408
|
if (name === PI_AGENT_NAME) {
|
|
414
|
-
config.active = PI_AGENT_NAME;
|
|
415
|
-
pm.saveConfig(config);
|
|
416
409
|
currentAgent = PI_AGENT_NAME;
|
|
417
410
|
await applyAgentSettings(ctx);
|
|
418
411
|
updateStatus(ctx);
|
|
@@ -425,8 +418,6 @@ export default function agentsSwitch(pi: ExtensionAPI) {
|
|
|
425
418
|
);
|
|
426
419
|
return;
|
|
427
420
|
}
|
|
428
|
-
config.active = name;
|
|
429
|
-
pm.saveConfig(config);
|
|
430
421
|
currentAgent = name;
|
|
431
422
|
await applyAgentSettings(ctx);
|
|
432
423
|
updateStatus(ctx);
|
|
@@ -702,7 +693,6 @@ export default function agentsSwitch(pi: ExtensionAPI) {
|
|
|
702
693
|
}
|
|
703
694
|
}
|
|
704
695
|
if (count > 0) {
|
|
705
|
-
loadCurrentAgent(pm.loadConfig());
|
|
706
696
|
ctx.ui.notify(`Initialized ${count} default agent(s)`, "info");
|
|
707
697
|
} else {
|
|
708
698
|
ctx.ui.notify("Default agents already initialized", "info");
|
|
@@ -728,7 +718,6 @@ export default function agentsSwitch(pi: ExtensionAPI) {
|
|
|
728
718
|
|
|
729
719
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
730
720
|
initPM(ctx.cwd);
|
|
731
|
-
loadCurrentAgent(pm.loadConfig());
|
|
732
721
|
updateStatus(ctx);
|
|
733
722
|
|
|
734
723
|
if (currentAgent === PI_AGENT_NAME) return;
|
|
@@ -771,20 +760,35 @@ export default function agentsSwitch(pi: ExtensionAPI) {
|
|
|
771
760
|
if (currentResolved && currentResolved.skillNames.length > 0) {
|
|
772
761
|
const loadedSkills = (event.systemPromptOptions as { skills?: any[] })
|
|
773
762
|
.skills;
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
description: string;
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
if (loadedSkills
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
763
|
+
const skillMap = new Map<
|
|
764
|
+
string,
|
|
765
|
+
{ name: string; description: string; filePath: string }
|
|
766
|
+
>();
|
|
767
|
+
|
|
768
|
+
// Collect from Pi's loaded skills first (richest data)
|
|
769
|
+
if (loadedSkills) {
|
|
770
|
+
for (const s of loadedSkills) {
|
|
771
|
+
skillMap.set(s.name, {
|
|
772
|
+
name: s.name,
|
|
773
|
+
description: s.description ?? "",
|
|
774
|
+
filePath: s.filePath ?? "",
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Fill gaps from disk (agent-specific skills not in Pi's loaded list)
|
|
780
|
+
for (const name of currentResolved.skillNames) {
|
|
781
|
+
if (!skillMap.has(name)) {
|
|
782
|
+
const found = pm.getSkillObjects([name]);
|
|
783
|
+
if (found.length > 0) skillMap.set(found[0].name, found[0]);
|
|
784
|
+
}
|
|
786
785
|
}
|
|
787
786
|
|
|
787
|
+
// Filter to only resolved names, preserving order
|
|
788
|
+
const selectedSkills = currentResolved.skillNames
|
|
789
|
+
.map((name) => skillMap.get(name))
|
|
790
|
+
.filter((s): s is NonNullable<typeof s> => s != null);
|
|
791
|
+
|
|
788
792
|
if (selectedSkills.length > 0) {
|
|
789
793
|
const skillsBlock = formatSkillsBlock(selectedSkills);
|
|
790
794
|
const currentDateMarker = "\nCurrent date:";
|
|
@@ -808,7 +812,6 @@ export default function agentsSwitch(pi: ExtensionAPI) {
|
|
|
808
812
|
const payload = event.payload as Record<string, unknown>;
|
|
809
813
|
let changed = false;
|
|
810
814
|
|
|
811
|
-
// Temperature / topP
|
|
812
815
|
if (currentResolved.temperature !== undefined) {
|
|
813
816
|
payload.temperature = currentResolved.temperature;
|
|
814
817
|
changed = true;
|
|
@@ -818,12 +821,8 @@ export default function agentsSwitch(pi: ExtensionAPI) {
|
|
|
818
821
|
changed = true;
|
|
819
822
|
}
|
|
820
823
|
|
|
821
|
-
//
|
|
822
|
-
//
|
|
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
|
|
824
|
+
// Filter tools at the API level — last defence against other
|
|
825
|
+
// extensions (e.g. pi-lens) overriding our tool set.
|
|
827
826
|
const allowedTools = new Set(currentResolved.tools);
|
|
828
827
|
const rawTools = payload.tools as
|
|
829
828
|
| Array<{ function?: { name?: string } }>
|
|
@@ -836,104 +835,6 @@ export default function agentsSwitch(pi: ExtensionAPI) {
|
|
|
836
835
|
if ((payload.tools as Array<any>).length !== before) changed = true;
|
|
837
836
|
}
|
|
838
837
|
|
|
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((t) => t.name === name);
|
|
871
|
-
return tool
|
|
872
|
-
? `- ${tool.name}: ${tool.description ?? ""}`
|
|
873
|
-
: `- ${name}`;
|
|
874
|
-
})
|
|
875
|
-
.join("\n");
|
|
876
|
-
content =
|
|
877
|
-
content.substring(0, toolsIdx) +
|
|
878
|
-
`\nAvailable tools:\n${cleanLines}\n` +
|
|
879
|
-
content.substring(nextSection);
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
|
|
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
|
-
}
|
|
898
|
-
|
|
899
|
-
// Rebuild clean skills block from our resolved config
|
|
900
|
-
const selectedSkills = pm.getSkillObjects(currentResolved.skillNames);
|
|
901
|
-
if (selectedSkills.length > 0) {
|
|
902
|
-
const skillsBlock = formatSkillsBlock(selectedSkills);
|
|
903
|
-
// Insert before "Current date:"
|
|
904
|
-
const currentDateMarker = "\nCurrent date:";
|
|
905
|
-
const idx = content.indexOf(currentDateMarker);
|
|
906
|
-
if (idx >= 0) {
|
|
907
|
-
content =
|
|
908
|
-
content.substring(0, idx) + skillsBlock + content.substring(idx);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// Strip "Pi documentation" block (pi-lens may re-add after our handler strips it)
|
|
914
|
-
const piDocsMarker =
|
|
915
|
-
"Pi documentation (read only when the user asks about pi itself";
|
|
916
|
-
const piDocsStart = content.indexOf(piDocsMarker);
|
|
917
|
-
if (piDocsStart >= 0) {
|
|
918
|
-
const currentDateMarker = "\nCurrent date:";
|
|
919
|
-
const currentDateStart = content.indexOf(
|
|
920
|
-
currentDateMarker,
|
|
921
|
-
piDocsStart,
|
|
922
|
-
);
|
|
923
|
-
if (currentDateStart >= 0) {
|
|
924
|
-
content =
|
|
925
|
-
content.substring(0, piDocsStart) +
|
|
926
|
-
content.substring(currentDateStart);
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
if (content.length !== originalLen) {
|
|
931
|
-
messages[0].content = content;
|
|
932
|
-
changed = true;
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
// Return payload only if we changed something (otherwise Pi uses default)
|
|
937
838
|
if (changed) return payload;
|
|
938
839
|
});
|
|
939
840
|
|
|
@@ -941,8 +842,15 @@ export default function agentsSwitch(pi: ExtensionAPI) {
|
|
|
941
842
|
|
|
942
843
|
pi.on("session_start", async (_event, ctx) => {
|
|
943
844
|
initPM(ctx.cwd);
|
|
944
|
-
|
|
945
|
-
|
|
845
|
+
|
|
846
|
+
// Only restore from config on the very first session_start
|
|
847
|
+
// within this process. After that, use in-memory state.
|
|
848
|
+
if (!sessionInitialized) {
|
|
849
|
+
const config = pm.loadConfig();
|
|
850
|
+
if (config.default) loadCurrentAgent(config);
|
|
851
|
+
sessionInitialized = true;
|
|
852
|
+
}
|
|
853
|
+
|
|
946
854
|
// Apply agent settings immediately so tools/model/thinking
|
|
947
855
|
// are set before the first prompt builds the system prompt.
|
|
948
856
|
await applyAgentSettings(ctx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-agents-switch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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": {
|
package/profile-manager.ts
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
* Profile dir layout:
|
|
9
9
|
* ~/.pi/agents/<name>/ ← user-level agent
|
|
10
10
|
* ├── AGENTS.md ← YAML frontmatter + system prompt
|
|
11
|
-
* ├── extensions/ ← agent-specific extensions
|
|
12
11
|
* ├── skills/ ← agent-specific skills
|
|
13
12
|
* └── prompts/ ← agent-specific prompt templates
|
|
14
13
|
*
|
|
@@ -21,7 +20,7 @@
|
|
|
21
20
|
* 3. PI defaults (from running pi environment)
|
|
22
21
|
*
|
|
23
22
|
* Last-write-wins: when the same item appears in both tools and
|
|
24
|
-
* excluded_tools (or
|
|
23
|
+
* excluded_tools (or skills/excluded_skills),
|
|
25
24
|
* whichever YAML key was declared LAST in the file wins.
|
|
26
25
|
* `"*"` in any exclude list removes everything except items explicitly added.
|
|
27
26
|
*/
|
|
@@ -66,7 +65,10 @@ export class ProfileManager {
|
|
|
66
65
|
readonly piAgentDir: string;
|
|
67
66
|
|
|
68
67
|
/** Cached skill index keyed by name (rebuilt on cwd change) */
|
|
69
|
-
private skillCache: Map<
|
|
68
|
+
private skillCache: Map<
|
|
69
|
+
string,
|
|
70
|
+
{ name: string; description: string; filePath: string; source: string }
|
|
71
|
+
> | null = null;
|
|
70
72
|
|
|
71
73
|
constructor(private cwd?: string) {
|
|
72
74
|
const piRoot = join(homedir(), ".pi");
|
|
@@ -93,9 +95,14 @@ export class ProfileManager {
|
|
|
93
95
|
|
|
94
96
|
try {
|
|
95
97
|
const raw = readFileSync(this.configPath, "utf8");
|
|
96
|
-
const cfg = JSON.parse(raw) as AgentsConfig;
|
|
98
|
+
const cfg = JSON.parse(raw) as AgentsConfig & { active?: string };
|
|
97
99
|
if (cfg.version !== 1) throw new Error("Unsupported config version");
|
|
98
|
-
|
|
100
|
+
// Migrate from deprecated `active` to `default`
|
|
101
|
+
if (cfg.active !== undefined && cfg.default === undefined) {
|
|
102
|
+
cfg.default = cfg.active;
|
|
103
|
+
this.saveConfig(cfg as AgentsConfig);
|
|
104
|
+
}
|
|
105
|
+
return cfg as AgentsConfig;
|
|
99
106
|
} catch (err) {
|
|
100
107
|
const corruptPath = this.configPath + ".corrupted." + Date.now();
|
|
101
108
|
try {
|
|
@@ -159,7 +166,6 @@ export class ProfileManager {
|
|
|
159
166
|
if (existing) {
|
|
160
167
|
existing.path = path;
|
|
161
168
|
existing.hasAgentsMd = existsSync(join(path, "AGENTS.md"));
|
|
162
|
-
existing.hasExtensions = existsSync(join(path, "extensions"));
|
|
163
169
|
existing.hasSkills = existsSync(join(path, "skills"));
|
|
164
170
|
existing.source = "project";
|
|
165
171
|
} else {
|
|
@@ -180,7 +186,6 @@ export class ProfileManager {
|
|
|
180
186
|
name,
|
|
181
187
|
path,
|
|
182
188
|
hasAgentsMd: existsSync(join(path, "AGENTS.md")),
|
|
183
|
-
hasExtensions: existsSync(join(path, "extensions")),
|
|
184
189
|
hasSkills: existsSync(join(path, "skills")),
|
|
185
190
|
source,
|
|
186
191
|
};
|
|
@@ -354,24 +359,34 @@ export class ProfileManager {
|
|
|
354
359
|
keyOrder,
|
|
355
360
|
);
|
|
356
361
|
|
|
357
|
-
// Skills:
|
|
358
|
-
const
|
|
359
|
-
|
|
362
|
+
// Skills: resolve PI's base, then auto-append agent's own skills
|
|
363
|
+
const agentSkillNames = this.getAgentSkillNames(
|
|
364
|
+
name,
|
|
365
|
+
agentPath,
|
|
366
|
+
projectPath,
|
|
367
|
+
);
|
|
368
|
+
const resolvedSkills =
|
|
360
369
|
fm.excluded_skills !== undefined
|
|
361
|
-
?
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
370
|
+
? this.resolveList(
|
|
371
|
+
pi.skills,
|
|
372
|
+
fm.excluded_skills,
|
|
373
|
+
fm.skills,
|
|
374
|
+
"skills",
|
|
375
|
+
"excluded_skills",
|
|
376
|
+
keyOrder,
|
|
377
|
+
)
|
|
378
|
+
: [...pi.skills, ...(fm.skills ?? [])];
|
|
379
|
+
|
|
380
|
+
// Agent skills are always available, unless explicitly excluded by name
|
|
381
|
+
const excludedSet = new Set(fm.excluded_skills ?? []);
|
|
382
|
+
const resolvedSet = new Set(resolvedSkills);
|
|
383
|
+
for (const skill of agentSkillNames) {
|
|
384
|
+
if (!excludedSet.has(skill) && !resolvedSet.has(skill)) {
|
|
385
|
+
resolvedSkills.push(skill);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const skillPaths = resolvedSkills;
|
|
375
390
|
|
|
376
391
|
const sourcePath = projectPath ?? agentPath ?? "PI (default)";
|
|
377
392
|
|
|
@@ -463,6 +478,63 @@ export class ProfileManager {
|
|
|
463
478
|
* Look up full skill objects by name from Pi's standard skill directories.
|
|
464
479
|
* Returns name, description, and filePath for each skill found.
|
|
465
480
|
*/
|
|
481
|
+
/** Get PI skill names (global + extension + project, excluding agent-scoped). */
|
|
482
|
+
getPISkillNames(): string[] {
|
|
483
|
+
if (!this.skillCache) this.rebuildSkillIndex();
|
|
484
|
+
return [...this.skillCache!.entries()]
|
|
485
|
+
.filter(([, v]) => v.source !== "agent")
|
|
486
|
+
.map(([k]) => k);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Discover skill names from an agent's own skills/ directory.
|
|
491
|
+
* Scans both user-level (~/.pi/agents/<name>/skills/) and
|
|
492
|
+
* project-level (<cwd>/.pi/agents/<name>/skills/) locations.
|
|
493
|
+
*/
|
|
494
|
+
getAgentSkillNames(
|
|
495
|
+
name: string,
|
|
496
|
+
agentPath?: string,
|
|
497
|
+
projectPath?: string,
|
|
498
|
+
): string[] {
|
|
499
|
+
const names = new Set<string>();
|
|
500
|
+
|
|
501
|
+
const scanDir = (dir: string): boolean => {
|
|
502
|
+
if (!existsSync(dir)) return false;
|
|
503
|
+
try {
|
|
504
|
+
for (const entry of readdirSync(dir)) {
|
|
505
|
+
const full = join(dir, entry);
|
|
506
|
+
try {
|
|
507
|
+
if (
|
|
508
|
+
statSync(full).isDirectory() &&
|
|
509
|
+
existsSync(join(full, "SKILL.md"))
|
|
510
|
+
) {
|
|
511
|
+
names.add(entry);
|
|
512
|
+
}
|
|
513
|
+
} catch {
|
|
514
|
+
// skip
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return true;
|
|
518
|
+
} catch {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// User-level agent skills
|
|
524
|
+
const userSkillsDir = join(this.agentPath(name), "skills");
|
|
525
|
+
scanDir(userSkillsDir);
|
|
526
|
+
|
|
527
|
+
// Project-level agent skills (if known)
|
|
528
|
+
if (projectPath) {
|
|
529
|
+
scanDir(join(projectPath, "skills"));
|
|
530
|
+
} else if (agentPath && agentPath !== this.agentPath(name)) {
|
|
531
|
+
// agentPath might already be the project path
|
|
532
|
+
scanDir(join(agentPath, "skills"));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return [...names];
|
|
536
|
+
}
|
|
537
|
+
|
|
466
538
|
/**
|
|
467
539
|
* Look up full skill objects by name.
|
|
468
540
|
* Scans Pi's standard skill locations recursively (flat + nested).
|
|
@@ -505,7 +577,7 @@ export class ProfileManager {
|
|
|
505
577
|
rebuildSkillIndex(): void {
|
|
506
578
|
this.skillCache = new Map();
|
|
507
579
|
|
|
508
|
-
const scanDir = (dir: string, maxDepth: number): void => {
|
|
580
|
+
const scanDir = (dir: string, maxDepth: number, source: string): void => {
|
|
509
581
|
if (maxDepth <= 0) return;
|
|
510
582
|
|
|
511
583
|
// Check for SKILL.md in this directory
|
|
@@ -515,37 +587,28 @@ export class ProfileManager {
|
|
|
515
587
|
const content = readFileSync(skillMd, "utf8");
|
|
516
588
|
const { frontmatter } = parseFrontmatter(content);
|
|
517
589
|
const skillName = frontmatter.name ?? basename(dir);
|
|
518
|
-
// Last-write-wins: later scan locations override earlier ones
|
|
519
590
|
this.skillCache!.set(skillName, {
|
|
520
591
|
name: skillName,
|
|
521
592
|
description: frontmatter.description ?? "",
|
|
522
593
|
filePath: skillMd,
|
|
594
|
+
source,
|
|
523
595
|
});
|
|
524
|
-
} catch {
|
|
525
|
-
// Skip unreadable files
|
|
526
|
-
}
|
|
596
|
+
} catch {}
|
|
527
597
|
}
|
|
528
598
|
|
|
529
|
-
// Recurse into subdirectories
|
|
530
599
|
try {
|
|
531
600
|
for (const entry of readdirSync(dir)) {
|
|
532
|
-
if (entry === "node_modules") continue;
|
|
601
|
+
if (entry === "node_modules") continue;
|
|
533
602
|
const sub = join(dir, entry);
|
|
534
603
|
try {
|
|
535
|
-
if (statSync(sub).isDirectory())
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
} catch {
|
|
539
|
-
// Skip unreadable entries
|
|
540
|
-
}
|
|
604
|
+
if (statSync(sub).isDirectory()) scanDir(sub, maxDepth - 1, source);
|
|
605
|
+
} catch {}
|
|
541
606
|
}
|
|
542
|
-
} catch {
|
|
543
|
-
// Skip unreadable directories
|
|
544
|
-
}
|
|
607
|
+
} catch {}
|
|
545
608
|
};
|
|
546
609
|
|
|
547
|
-
// 1. Global Pi skills
|
|
548
|
-
scanDir(join(this.piAgentDir, "skills"), 2);
|
|
610
|
+
// 1. Global Pi skills
|
|
611
|
+
scanDir(join(this.piAgentDir, "skills"), 2, "global");
|
|
549
612
|
|
|
550
613
|
// 2. Extension skills from npm packages (nested: node_modules/<ext>/skills/<name>/SKILL.md)
|
|
551
614
|
const npmDir = join(this.piAgentDir, "npm", "node_modules");
|
|
@@ -555,7 +618,7 @@ export class ProfileManager {
|
|
|
555
618
|
if (existsSync(extSkills)) {
|
|
556
619
|
try {
|
|
557
620
|
if (statSync(extSkills).isDirectory()) {
|
|
558
|
-
scanDir(extSkills, 3);
|
|
621
|
+
scanDir(extSkills, 3, "extension");
|
|
559
622
|
}
|
|
560
623
|
} catch {
|
|
561
624
|
// Skip
|
|
@@ -573,7 +636,7 @@ export class ProfileManager {
|
|
|
573
636
|
if (existsSync(agentSkills)) {
|
|
574
637
|
try {
|
|
575
638
|
if (statSync(agentSkills).isDirectory()) {
|
|
576
|
-
scanDir(agentSkills, 3);
|
|
639
|
+
scanDir(agentSkills, 3, "agent");
|
|
577
640
|
}
|
|
578
641
|
} catch {
|
|
579
642
|
// Skip
|
|
@@ -589,7 +652,7 @@ export class ProfileManager {
|
|
|
589
652
|
if (this.cwd) {
|
|
590
653
|
const projSkillsDir = join(this.cwd, ".pi", "skills");
|
|
591
654
|
if (existsSync(projSkillsDir)) {
|
|
592
|
-
scanDir(projSkillsDir, 2);
|
|
655
|
+
scanDir(projSkillsDir, 2, "project");
|
|
593
656
|
}
|
|
594
657
|
}
|
|
595
658
|
}
|
|
@@ -621,7 +684,6 @@ export class ProfileManager {
|
|
|
621
684
|
// Build directory structure
|
|
622
685
|
try {
|
|
623
686
|
mkdirSync(path, { recursive: true });
|
|
624
|
-
mkdirSync(join(path, "extensions"));
|
|
625
687
|
mkdirSync(join(path, "skills"));
|
|
626
688
|
mkdirSync(join(path, "prompts"));
|
|
627
689
|
} catch (err) {
|
|
@@ -669,8 +731,8 @@ Edit this file to customize your behavior.
|
|
|
669
731
|
rmSync(path, { recursive: true, force: true });
|
|
670
732
|
|
|
671
733
|
const config = this.loadConfig();
|
|
672
|
-
if (config.
|
|
673
|
-
delete config.
|
|
734
|
+
if (config.default === name) {
|
|
735
|
+
delete config.default;
|
|
674
736
|
}
|
|
675
737
|
this.saveConfig(config);
|
|
676
738
|
}
|
|
@@ -682,8 +744,8 @@ Edit this file to customize your behavior.
|
|
|
682
744
|
* Falls back to "PI" if nothing is set or the configured agent no longer exists.
|
|
683
745
|
*/
|
|
684
746
|
getActive(config: AgentsConfig): string {
|
|
685
|
-
if (config.
|
|
686
|
-
return config.
|
|
747
|
+
if (config.default && this.exists(config.default)) {
|
|
748
|
+
return config.default;
|
|
687
749
|
}
|
|
688
750
|
return "PI";
|
|
689
751
|
}
|
package/types.ts
CHANGED
|
@@ -51,11 +51,8 @@ export interface AgentFrontmatter {
|
|
|
51
51
|
skills?: string[];
|
|
52
52
|
/**
|
|
53
53
|
* Skills to REMOVE (from PI's inherited set). Supports `"*"` wildcard.
|
|
54
|
-
* Preferred over deprecated `noskills`.
|
|
55
54
|
*/
|
|
56
55
|
excluded_skills?: string[];
|
|
57
|
-
/** @deprecated Use `excluded_skills` instead. Still recognized for backward compat. */
|
|
58
|
-
noskills?: string[];
|
|
59
56
|
}
|
|
60
57
|
|
|
61
58
|
// ─── Frontmatter parse result ──────────────────────────────
|
|
@@ -116,7 +113,7 @@ export interface AgentsConfig {
|
|
|
116
113
|
/** Key string like "f9" or "ctrl+shift+a" */
|
|
117
114
|
hotkey?: KeyId;
|
|
118
115
|
/** Which agent is currently active ("PI" or a custom agent name) */
|
|
119
|
-
|
|
116
|
+
default?: string;
|
|
120
117
|
}
|
|
121
118
|
|
|
122
119
|
// ─── Disk Profile ──────────────────────────────────────────
|
|
@@ -129,8 +126,6 @@ export interface AgentDiskProfile {
|
|
|
129
126
|
path: string;
|
|
130
127
|
/** Has an AGENTS.md file */
|
|
131
128
|
hasAgentsMd: boolean;
|
|
132
|
-
/** Has an extensions/ subdirectory */
|
|
133
|
-
hasExtensions: boolean;
|
|
134
129
|
/** Has a skills/ subdirectory */
|
|
135
130
|
hasSkills: boolean;
|
|
136
131
|
/** Agent source scope */
|