pi-agents-switch 0.2.7 → 0.3.0
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 +14 -130
- package/package.json +1 -1
- package/profile-manager.ts +195 -43
- 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;
|
|
@@ -808,7 +797,6 @@ export default function agentsSwitch(pi: ExtensionAPI) {
|
|
|
808
797
|
const payload = event.payload as Record<string, unknown>;
|
|
809
798
|
let changed = false;
|
|
810
799
|
|
|
811
|
-
// Temperature / topP
|
|
812
800
|
if (currentResolved.temperature !== undefined) {
|
|
813
801
|
payload.temperature = currentResolved.temperature;
|
|
814
802
|
changed = true;
|
|
@@ -818,12 +806,8 @@ export default function agentsSwitch(pi: ExtensionAPI) {
|
|
|
818
806
|
changed = true;
|
|
819
807
|
}
|
|
820
808
|
|
|
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
|
|
809
|
+
// Filter tools at the API level — last defence against other
|
|
810
|
+
// extensions (e.g. pi-lens) overriding our tool set.
|
|
827
811
|
const allowedTools = new Set(currentResolved.tools);
|
|
828
812
|
const rawTools = payload.tools as
|
|
829
813
|
| Array<{ function?: { name?: string } }>
|
|
@@ -836,113 +820,6 @@ export default function agentsSwitch(pi: ExtensionAPI) {
|
|
|
836
820
|
if ((payload.tools as Array<any>).length !== before) changed = true;
|
|
837
821
|
}
|
|
838
822
|
|
|
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(
|
|
892
|
-
"\n</available_skills>",
|
|
893
|
-
skillsIdx,
|
|
894
|
-
);
|
|
895
|
-
if (skillsEnd >= 0) {
|
|
896
|
-
content =
|
|
897
|
-
content.substring(0, skillsIdx) +
|
|
898
|
-
content.substring(
|
|
899
|
-
skillsEnd + "\n</available_skills>".length,
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
// Rebuild clean skills block from our resolved config
|
|
905
|
-
const selectedSkills = pm.getSkillObjects(
|
|
906
|
-
currentResolved.skillNames,
|
|
907
|
-
);
|
|
908
|
-
if (selectedSkills.length > 0) {
|
|
909
|
-
const skillsBlock = formatSkillsBlock(selectedSkills);
|
|
910
|
-
// Insert before "Current date:"
|
|
911
|
-
const currentDateMarker = "\nCurrent date:";
|
|
912
|
-
const idx = content.indexOf(currentDateMarker);
|
|
913
|
-
if (idx >= 0) {
|
|
914
|
-
content =
|
|
915
|
-
content.substring(0, idx) +
|
|
916
|
-
skillsBlock +
|
|
917
|
-
content.substring(idx);
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
// Strip "Pi documentation" block (pi-lens may re-add after our handler strips it)
|
|
923
|
-
const piDocsMarker =
|
|
924
|
-
"Pi documentation (read only when the user asks about pi itself";
|
|
925
|
-
const piDocsStart = content.indexOf(piDocsMarker);
|
|
926
|
-
if (piDocsStart >= 0) {
|
|
927
|
-
const currentDateMarker = "\nCurrent date:";
|
|
928
|
-
const currentDateStart = content.indexOf(
|
|
929
|
-
currentDateMarker,
|
|
930
|
-
piDocsStart,
|
|
931
|
-
);
|
|
932
|
-
if (currentDateStart >= 0) {
|
|
933
|
-
content =
|
|
934
|
-
content.substring(0, piDocsStart) +
|
|
935
|
-
content.substring(currentDateStart);
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
if (content.length !== originalLen) {
|
|
940
|
-
messages[0].content = content;
|
|
941
|
-
changed = true;
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
// Return payload only if we changed something (otherwise Pi uses default)
|
|
946
823
|
if (changed) return payload;
|
|
947
824
|
});
|
|
948
825
|
|
|
@@ -950,8 +827,15 @@ export default function agentsSwitch(pi: ExtensionAPI) {
|
|
|
950
827
|
|
|
951
828
|
pi.on("session_start", async (_event, ctx) => {
|
|
952
829
|
initPM(ctx.cwd);
|
|
953
|
-
|
|
954
|
-
|
|
830
|
+
|
|
831
|
+
// Only restore from config on the very first session_start
|
|
832
|
+
// within this process. After that, use in-memory state.
|
|
833
|
+
if (!sessionInitialized) {
|
|
834
|
+
const config = pm.loadConfig();
|
|
835
|
+
if (config.default) loadCurrentAgent(config);
|
|
836
|
+
sessionInitialized = true;
|
|
837
|
+
}
|
|
838
|
+
|
|
955
839
|
// Apply agent settings immediately so tools/model/thinking
|
|
956
840
|
// are set before the first prompt builds the system prompt.
|
|
957
841
|
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.0",
|
|
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
|
*/
|
|
@@ -35,7 +34,7 @@ import {
|
|
|
35
34
|
statSync,
|
|
36
35
|
rmSync,
|
|
37
36
|
} from "node:fs";
|
|
38
|
-
import { join } from "node:path";
|
|
37
|
+
import { join, basename } from "node:path";
|
|
39
38
|
import { homedir } from "node:os";
|
|
40
39
|
import { parseFrontmatter, serializeFrontmatter } from "./frontmatter";
|
|
41
40
|
import type {
|
|
@@ -65,6 +64,12 @@ export class ProfileManager {
|
|
|
65
64
|
readonly configPath: string;
|
|
66
65
|
readonly piAgentDir: string;
|
|
67
66
|
|
|
67
|
+
/** Cached skill index keyed by name (rebuilt on cwd change) */
|
|
68
|
+
private skillCache: Map<
|
|
69
|
+
string,
|
|
70
|
+
{ name: string; description: string; filePath: string; source: string }
|
|
71
|
+
> | null = null;
|
|
72
|
+
|
|
68
73
|
constructor(private cwd?: string) {
|
|
69
74
|
const piRoot = join(homedir(), ".pi");
|
|
70
75
|
this.userAgentsDir = join(piRoot, "agents");
|
|
@@ -90,9 +95,14 @@ export class ProfileManager {
|
|
|
90
95
|
|
|
91
96
|
try {
|
|
92
97
|
const raw = readFileSync(this.configPath, "utf8");
|
|
93
|
-
const cfg = JSON.parse(raw) as AgentsConfig;
|
|
98
|
+
const cfg = JSON.parse(raw) as AgentsConfig & { active?: string };
|
|
94
99
|
if (cfg.version !== 1) throw new Error("Unsupported config version");
|
|
95
|
-
|
|
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;
|
|
96
106
|
} catch (err) {
|
|
97
107
|
const corruptPath = this.configPath + ".corrupted." + Date.now();
|
|
98
108
|
try {
|
|
@@ -156,7 +166,6 @@ export class ProfileManager {
|
|
|
156
166
|
if (existing) {
|
|
157
167
|
existing.path = path;
|
|
158
168
|
existing.hasAgentsMd = existsSync(join(path, "AGENTS.md"));
|
|
159
|
-
existing.hasExtensions = existsSync(join(path, "extensions"));
|
|
160
169
|
existing.hasSkills = existsSync(join(path, "skills"));
|
|
161
170
|
existing.source = "project";
|
|
162
171
|
} else {
|
|
@@ -177,7 +186,6 @@ export class ProfileManager {
|
|
|
177
186
|
name,
|
|
178
187
|
path,
|
|
179
188
|
hasAgentsMd: existsSync(join(path, "AGENTS.md")),
|
|
180
|
-
hasExtensions: existsSync(join(path, "extensions")),
|
|
181
189
|
hasSkills: existsSync(join(path, "skills")),
|
|
182
190
|
source,
|
|
183
191
|
};
|
|
@@ -351,24 +359,34 @@ export class ProfileManager {
|
|
|
351
359
|
keyOrder,
|
|
352
360
|
);
|
|
353
361
|
|
|
354
|
-
// Skills:
|
|
355
|
-
const
|
|
356
|
-
|
|
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 =
|
|
357
369
|
fm.excluded_skills !== undefined
|
|
358
|
-
?
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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;
|
|
372
390
|
|
|
373
391
|
const sourcePath = projectPath ?? agentPath ?? "PI (default)";
|
|
374
392
|
|
|
@@ -460,6 +478,67 @@ export class ProfileManager {
|
|
|
460
478
|
* Look up full skill objects by name from Pi's standard skill directories.
|
|
461
479
|
* Returns name, description, and filePath for each skill found.
|
|
462
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
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Look up full skill objects by name.
|
|
540
|
+
* Scans Pi's standard skill locations recursively (flat + nested).
|
|
541
|
+
*/
|
|
463
542
|
getSkillObjects(
|
|
464
543
|
skillNames: string[],
|
|
465
544
|
): Array<{ name: string; description: string; filePath: string }> {
|
|
@@ -480,28 +559,102 @@ export class ProfileManager {
|
|
|
480
559
|
private findSkillOnDisk(
|
|
481
560
|
name: string,
|
|
482
561
|
): { name: string; description: string; filePath: string } | undefined {
|
|
483
|
-
|
|
562
|
+
if (!this.skillCache) {
|
|
563
|
+
this.rebuildSkillIndex();
|
|
564
|
+
}
|
|
565
|
+
return this.skillCache!.get(name);
|
|
566
|
+
}
|
|
484
567
|
|
|
485
|
-
|
|
486
|
-
|
|
568
|
+
/**
|
|
569
|
+
* Rebuild the skill index by scanning all known skill directories.
|
|
570
|
+
*
|
|
571
|
+
* Locations scanned:
|
|
572
|
+
* 1. ~/.pi/agent/skills/<name>/SKILL.md (flat, user-installed)
|
|
573
|
+
* 2. ~/.pi/agent/npm/node_modules/<ext>/skills/ (extension skills)
|
|
574
|
+
* 3. ~/.pi/agents/<agent>/skills/ (agent-specific skills)
|
|
575
|
+
* 4. <cwd>/.pi/skills/ (project skills, flat)
|
|
576
|
+
*/
|
|
577
|
+
rebuildSkillIndex(): void {
|
|
578
|
+
this.skillCache = new Map();
|
|
579
|
+
|
|
580
|
+
const scanDir = (dir: string, maxDepth: number, source: string): void => {
|
|
581
|
+
if (maxDepth <= 0) return;
|
|
582
|
+
|
|
583
|
+
// Check for SKILL.md in this directory
|
|
584
|
+
const skillMd = join(dir, "SKILL.md");
|
|
585
|
+
if (existsSync(skillMd)) {
|
|
586
|
+
try {
|
|
587
|
+
const content = readFileSync(skillMd, "utf8");
|
|
588
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
589
|
+
const skillName = frontmatter.name ?? basename(dir);
|
|
590
|
+
this.skillCache!.set(skillName, {
|
|
591
|
+
name: skillName,
|
|
592
|
+
description: frontmatter.description ?? "",
|
|
593
|
+
filePath: skillMd,
|
|
594
|
+
source,
|
|
595
|
+
});
|
|
596
|
+
} catch {}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
try {
|
|
600
|
+
for (const entry of readdirSync(dir)) {
|
|
601
|
+
if (entry === "node_modules") continue;
|
|
602
|
+
const sub = join(dir, entry);
|
|
603
|
+
try {
|
|
604
|
+
if (statSync(sub).isDirectory()) scanDir(sub, maxDepth - 1, source);
|
|
605
|
+
} catch {}
|
|
606
|
+
}
|
|
607
|
+
} catch {}
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
// 1. Global Pi skills
|
|
611
|
+
scanDir(join(this.piAgentDir, "skills"), 2, "global");
|
|
612
|
+
|
|
613
|
+
// 2. Extension skills from npm packages (nested: node_modules/<ext>/skills/<name>/SKILL.md)
|
|
614
|
+
const npmDir = join(this.piAgentDir, "npm", "node_modules");
|
|
615
|
+
if (existsSync(npmDir)) {
|
|
616
|
+
for (const ext of readdirSync(npmDir)) {
|
|
617
|
+
const extSkills = join(npmDir, ext, "skills");
|
|
618
|
+
if (existsSync(extSkills)) {
|
|
619
|
+
try {
|
|
620
|
+
if (statSync(extSkills).isDirectory()) {
|
|
621
|
+
scanDir(extSkills, 3, "extension");
|
|
622
|
+
}
|
|
623
|
+
} catch {
|
|
624
|
+
// Skip
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
487
628
|
}
|
|
488
629
|
|
|
489
|
-
|
|
490
|
-
|
|
630
|
+
// 3. Agent-specific skills (nested: agents/<agent>/skills/<name>/SKILL.md)
|
|
631
|
+
const agentsDir = join(homedir(), ".pi", "agents");
|
|
632
|
+
if (existsSync(agentsDir)) {
|
|
491
633
|
try {
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
634
|
+
for (const agent of readdirSync(agentsDir)) {
|
|
635
|
+
const agentSkills = join(agentsDir, agent, "skills");
|
|
636
|
+
if (existsSync(agentSkills)) {
|
|
637
|
+
try {
|
|
638
|
+
if (statSync(agentSkills).isDirectory()) {
|
|
639
|
+
scanDir(agentSkills, 3, "agent");
|
|
640
|
+
}
|
|
641
|
+
} catch {
|
|
642
|
+
// Skip
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
499
646
|
} catch {
|
|
500
|
-
// Skip unreadable
|
|
647
|
+
// Skip unreadable agents dir
|
|
501
648
|
}
|
|
502
649
|
}
|
|
503
650
|
|
|
504
|
-
|
|
651
|
+
// 4. Project-level skills (flat)
|
|
652
|
+
if (this.cwd) {
|
|
653
|
+
const projSkillsDir = join(this.cwd, ".pi", "skills");
|
|
654
|
+
if (existsSync(projSkillsDir)) {
|
|
655
|
+
scanDir(projSkillsDir, 2, "project");
|
|
656
|
+
}
|
|
657
|
+
}
|
|
505
658
|
}
|
|
506
659
|
|
|
507
660
|
// ─── Agent creation / deletion ───────────────────────
|
|
@@ -531,7 +684,6 @@ export class ProfileManager {
|
|
|
531
684
|
// Build directory structure
|
|
532
685
|
try {
|
|
533
686
|
mkdirSync(path, { recursive: true });
|
|
534
|
-
mkdirSync(join(path, "extensions"));
|
|
535
687
|
mkdirSync(join(path, "skills"));
|
|
536
688
|
mkdirSync(join(path, "prompts"));
|
|
537
689
|
} catch (err) {
|
|
@@ -579,8 +731,8 @@ Edit this file to customize your behavior.
|
|
|
579
731
|
rmSync(path, { recursive: true, force: true });
|
|
580
732
|
|
|
581
733
|
const config = this.loadConfig();
|
|
582
|
-
if (config.
|
|
583
|
-
delete config.
|
|
734
|
+
if (config.default === name) {
|
|
735
|
+
delete config.default;
|
|
584
736
|
}
|
|
585
737
|
this.saveConfig(config);
|
|
586
738
|
}
|
|
@@ -592,8 +744,8 @@ Edit this file to customize your behavior.
|
|
|
592
744
|
* Falls back to "PI" if nothing is set or the configured agent no longer exists.
|
|
593
745
|
*/
|
|
594
746
|
getActive(config: AgentsConfig): string {
|
|
595
|
-
if (config.
|
|
596
|
-
return config.
|
|
747
|
+
if (config.default && this.exists(config.default)) {
|
|
748
|
+
return config.default;
|
|
597
749
|
}
|
|
598
750
|
return "PI";
|
|
599
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 */
|