@zaganjade/pi-multi-skill 1.3.0 → 1.3.2
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 +50 -8
- package/package.json +1 -1
- package/skill-bundles.example.json +17 -17
- package/src/bmad-auto.ts +99 -99
- package/src/build.ts +352 -270
- package/src/bundles.ts +138 -138
- package/src/discover.ts +161 -161
- package/src/index.ts +13 -17
- package/src/metadata.ts +170 -170
- package/src/parse-args.ts +80 -80
- package/src/registry.ts +46 -46
- package/src/suggestions.ts +70 -70
- package/src/types.ts +64 -64
package/src/build.ts
CHANGED
|
@@ -1,270 +1,352 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import type { BuildOptions, EnrichedSkillInfo, LoadMode } from "./types.ts";
|
|
5
|
-
import { stripFrontmatter } from "./metadata.ts";
|
|
6
|
-
import { buildParallelDispatchBlock } from "./subagents.ts";
|
|
7
|
-
|
|
8
|
-
const SUBAGENT_STOP = "<SUBAGENT-STOP>";
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
${
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
]
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
parts.push(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
parts.push(
|
|
209
|
-
parts.push(
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (
|
|
213
|
-
parts.push("");
|
|
214
|
-
parts.push(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (options.
|
|
223
|
-
parts.push("");
|
|
224
|
-
parts.push(
|
|
225
|
-
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import type { BuildOptions, EnrichedSkillInfo, LoadMode } from "./types.ts";
|
|
5
|
+
import { stripFrontmatter } from "./metadata.ts";
|
|
6
|
+
import { buildParallelDispatchBlock } from "./subagents.ts";
|
|
7
|
+
|
|
8
|
+
const SUBAGENT_STOP = "<SUBAGENT-STOP>";
|
|
9
|
+
const MULTI_SKILL_LOCATION = "pi-multi-skill";
|
|
10
|
+
const SKILL_CHECK_MARKERS = [
|
|
11
|
+
"If you think there is even a 1% chance a skill might apply",
|
|
12
|
+
"Invoke relevant or requested skills BEFORE any response",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
/** Pi TUI collapses user messages that match this envelope (see pi-coding-agent parseSkillBlock). */
|
|
16
|
+
const PI_SKILL_BLOCK_RE =
|
|
17
|
+
/^<skill name="([^"]+)" location="([^"]+)">\n([\s\S]*?)\n<\/skill>(?:\n\n([\s\S]+))?$/;
|
|
18
|
+
|
|
19
|
+
function getAgentDir(): string {
|
|
20
|
+
return join(homedir(), ".pi", "agent");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function escapeXml(text: string): string {
|
|
24
|
+
return text
|
|
25
|
+
.replace(/&/g, "&")
|
|
26
|
+
.replace(/"/g, """)
|
|
27
|
+
.replace(/</g, "<");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Wrap content in Pi's native skill block so the TUI shows `[skill] name`. */
|
|
31
|
+
export function formatPiSkillBlock(
|
|
32
|
+
name: string,
|
|
33
|
+
location: string,
|
|
34
|
+
content: string,
|
|
35
|
+
): string {
|
|
36
|
+
return `<skill name="${escapeXml(name)}" location="${escapeXml(location)}">\n${content}\n</skill>`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isPiSkillBlock(text: string): boolean {
|
|
40
|
+
return PI_SKILL_BLOCK_RE.test(text);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatCollapsedSkillName(skills: EnrichedSkillInfo[]): string {
|
|
44
|
+
const names = skills.map((s) => s.name);
|
|
45
|
+
if (names.length === 1) return names[0];
|
|
46
|
+
if (names.length <= 3) return names.join(", ");
|
|
47
|
+
return `${names.slice(0, 2).join(", ")} +${names.length - 2} more`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readSkillFile(skill: EnrichedSkillInfo, cwd: string): string | null {
|
|
51
|
+
try {
|
|
52
|
+
return readFileSync(skill.filePath, "utf-8");
|
|
53
|
+
} catch {
|
|
54
|
+
// fall through
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const dirs = [join(getAgentDir(), "skills"), join(cwd, ".pi", "skills")];
|
|
58
|
+
for (const dir of dirs) {
|
|
59
|
+
for (const candidate of [
|
|
60
|
+
join(dir, skill.name, "SKILL.md"),
|
|
61
|
+
join(dir, `${skill.name}.md`),
|
|
62
|
+
]) {
|
|
63
|
+
try {
|
|
64
|
+
return readFileSync(candidate, "utf-8");
|
|
65
|
+
} catch {
|
|
66
|
+
// continue
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function extractCommandsSection(body: string): string {
|
|
74
|
+
const match = body.match(
|
|
75
|
+
/## Available Commands[\s\S]*?(?=\n## |\n# |$)/i,
|
|
76
|
+
);
|
|
77
|
+
return match ? match[0].trim() : "";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatMetaBody(skill: EnrichedSkillInfo): string {
|
|
81
|
+
const commands = skill.metadata.commands.length
|
|
82
|
+
? `\n\nCommands: ${skill.metadata.commands.join(", ")}`
|
|
83
|
+
: "";
|
|
84
|
+
const commandsSection = extractCommandsSection(skill.body);
|
|
85
|
+
const sectionText = commandsSection ? `\n\n${commandsSection}` : "";
|
|
86
|
+
|
|
87
|
+
return `(load mode: meta)\n\n${skill.metadata.description}${commands}${sectionText}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function formatLazyBody(skill: EnrichedSkillInfo, body: string): string {
|
|
91
|
+
const baseDir = dirname(skill.filePath);
|
|
92
|
+
const intro = body
|
|
93
|
+
.split("\n")
|
|
94
|
+
.filter((l) => l.trim() && !l.startsWith("#"))
|
|
95
|
+
.slice(0, 8)
|
|
96
|
+
.join("\n");
|
|
97
|
+
|
|
98
|
+
return [
|
|
99
|
+
`(load mode: lazy)`,
|
|
100
|
+
`References are relative to ${baseDir}. Load reference files with \`read\` only when the active workflow step requires them.`,
|
|
101
|
+
"",
|
|
102
|
+
intro,
|
|
103
|
+
extractCommandsSection(body),
|
|
104
|
+
]
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function formatFullBody(skill: EnrichedSkillInfo, body: string): string {
|
|
110
|
+
const baseDir = dirname(skill.filePath);
|
|
111
|
+
return `References are relative to ${baseDir}.\n\n${body}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function dedupeBody(name: string, body: string, seen: Set<string>): string {
|
|
115
|
+
let result = body;
|
|
116
|
+
|
|
117
|
+
if (body.includes(SUBAGENT_STOP)) {
|
|
118
|
+
if (seen.has(SUBAGENT_STOP)) {
|
|
119
|
+
result = result.replace(
|
|
120
|
+
new RegExp(`${SUBAGENT_STOP}[\\s\\S]*?${SUBAGENT_STOP}`, "g"),
|
|
121
|
+
"",
|
|
122
|
+
);
|
|
123
|
+
} else {
|
|
124
|
+
seen.add(SUBAGENT_STOP);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (name !== "using-superpowers") {
|
|
129
|
+
for (const marker of SKILL_CHECK_MARKERS) {
|
|
130
|
+
if (result.includes(marker) && seen.has(marker)) {
|
|
131
|
+
result = result.replace(
|
|
132
|
+
new RegExp(`## Instruction Priority[\\s\\S]*?(?=\\n## |$)`, "i"),
|
|
133
|
+
"",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (result.includes(marker)) seen.add(marker);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return result.trim();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function renderSkillBlock(
|
|
144
|
+
skill: EnrichedSkillInfo,
|
|
145
|
+
cwd: string,
|
|
146
|
+
mode: LoadMode,
|
|
147
|
+
seenDedup: Set<string>,
|
|
148
|
+
): string | null {
|
|
149
|
+
const effectiveMode = skill.metadata.tokenBudget ?? mode;
|
|
150
|
+
const raw = readSkillFile(skill, cwd);
|
|
151
|
+
if (!raw) return null;
|
|
152
|
+
|
|
153
|
+
const body = dedupeBody(skill.name, stripFrontmatter(raw).trim(), seenDedup);
|
|
154
|
+
let content: string;
|
|
155
|
+
switch (effectiveMode) {
|
|
156
|
+
case "meta":
|
|
157
|
+
content = formatMetaBody(skill);
|
|
158
|
+
break;
|
|
159
|
+
case "lazy":
|
|
160
|
+
content = formatLazyBody(skill, body);
|
|
161
|
+
break;
|
|
162
|
+
default:
|
|
163
|
+
content = formatFullBody(skill, body);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return formatPiSkillBlock(skill.name, skill.filePath, content);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildAgentPayload(
|
|
170
|
+
skills: EnrichedSkillInfo[],
|
|
171
|
+
expandedBlocks: string[],
|
|
172
|
+
options: BuildOptions,
|
|
173
|
+
notFound: string[],
|
|
174
|
+
skippedDuplicates: string[],
|
|
175
|
+
): string {
|
|
176
|
+
const skillNames = skills.map((s) => s.name).join(", ");
|
|
177
|
+
const bundleAttr =
|
|
178
|
+
options.bundles && options.bundles.length > 0
|
|
179
|
+
? ` bundles="${options.bundles.map((b) => `@${b}`).join(",")}"`
|
|
180
|
+
: "";
|
|
181
|
+
const parts: string[] = [
|
|
182
|
+
`<manually_attached_skills count="${skills.length}"${bundleAttr}>`,
|
|
183
|
+
"The user activated multiple skills. Follow ALL of them before responding.",
|
|
184
|
+
"Priority: process skills → planning → implementation. User instructions override conflicts.",
|
|
185
|
+
`Skills: ${skillNames}`,
|
|
186
|
+
`Load mode: ${options.mode}`,
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
if (options.parallel) {
|
|
190
|
+
const tasks =
|
|
191
|
+
options.parallelTasks && options.parallelTasks.length > 0
|
|
192
|
+
? options.parallelTasks
|
|
193
|
+
: options.instructions
|
|
194
|
+
? [options.instructions]
|
|
195
|
+
: [];
|
|
196
|
+
parts.push(
|
|
197
|
+
buildParallelDispatchBlock({
|
|
198
|
+
tasks,
|
|
199
|
+
subagentAvailable: options.subagentAvailable ?? false,
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
parts.push("");
|
|
205
|
+
parts.push(...expandedBlocks);
|
|
206
|
+
|
|
207
|
+
if (options.bmadStatusBlock) {
|
|
208
|
+
parts.push("");
|
|
209
|
+
parts.push(options.bmadStatusBlock);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (options.embeddedCommand) {
|
|
213
|
+
parts.push("");
|
|
214
|
+
parts.push(
|
|
215
|
+
`<embedded_command>${options.embeddedCommand}</embedded_command>`,
|
|
216
|
+
);
|
|
217
|
+
parts.push(
|
|
218
|
+
"Execute the embedded command workflow as part of fulfilling the user request.",
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (options.instructions) {
|
|
223
|
+
parts.push("");
|
|
224
|
+
parts.push("<user_query>");
|
|
225
|
+
parts.push(options.instructions);
|
|
226
|
+
parts.push("</user_query>");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (notFound.length > 0) {
|
|
230
|
+
parts.push("");
|
|
231
|
+
parts.push(`> ⚠️ Skills not found: ${notFound.join(", ")}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (options.conflictWarnings?.length) {
|
|
235
|
+
parts.push("");
|
|
236
|
+
parts.push(`> ⚠️ Conflicts: ${options.conflictWarnings.join("; ")}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (skippedDuplicates.length > 0) {
|
|
240
|
+
parts.push("");
|
|
241
|
+
parts.push(`> ℹ️ Deduplicated: ${skippedDuplicates.join("; ")}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
parts.push("</manually_attached_skills>");
|
|
245
|
+
return parts.join("\n");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function shouldWrapForDisplay(
|
|
249
|
+
skills: EnrichedSkillInfo[],
|
|
250
|
+
options: BuildOptions,
|
|
251
|
+
): boolean {
|
|
252
|
+
if (skills.length > 1) return true;
|
|
253
|
+
if (options.parallel || options.bmadStatusBlock || options.embeddedCommand) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
if (options.instructions) return true;
|
|
257
|
+
if (options.bundles && options.bundles.length > 0) return true;
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function buildCombinedMessage(
|
|
262
|
+
skills: EnrichedSkillInfo[],
|
|
263
|
+
cwd: string,
|
|
264
|
+
options: BuildOptions,
|
|
265
|
+
): { message: string; notFound: string[]; skippedDuplicates: string[] } {
|
|
266
|
+
const expandedBlocks: string[] = [];
|
|
267
|
+
const notFound: string[] = [];
|
|
268
|
+
const seenDedup = new Set<string>();
|
|
269
|
+
const skippedDuplicates: string[] = [];
|
|
270
|
+
|
|
271
|
+
for (const skill of skills) {
|
|
272
|
+
const before = seenDedup.size;
|
|
273
|
+
const block = renderSkillBlock(skill, cwd, options.mode, seenDedup);
|
|
274
|
+
if (!block) {
|
|
275
|
+
notFound.push(skill.name);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (seenDedup.size > before && before > 0) {
|
|
279
|
+
skippedDuplicates.push(`${skill.name} (deduplicated sections)`);
|
|
280
|
+
}
|
|
281
|
+
expandedBlocks.push(block);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const payload = buildAgentPayload(
|
|
285
|
+
skills,
|
|
286
|
+
expandedBlocks,
|
|
287
|
+
options,
|
|
288
|
+
notFound,
|
|
289
|
+
skippedDuplicates,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
let message: string;
|
|
293
|
+
if (expandedBlocks.length === 0) {
|
|
294
|
+
message = payload;
|
|
295
|
+
} else if (shouldWrapForDisplay(skills, options)) {
|
|
296
|
+
message = formatPiSkillBlock(
|
|
297
|
+
formatCollapsedSkillName(skills),
|
|
298
|
+
skills.length === 1 ? skills[0].filePath : MULTI_SKILL_LOCATION,
|
|
299
|
+
payload,
|
|
300
|
+
);
|
|
301
|
+
} else {
|
|
302
|
+
message = expandedBlocks[0];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
message,
|
|
307
|
+
notFound,
|
|
308
|
+
skippedDuplicates,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function resolveAndReadLegacySkill(
|
|
313
|
+
skillName: string,
|
|
314
|
+
filePath: string,
|
|
315
|
+
cwd: string,
|
|
316
|
+
): string | null {
|
|
317
|
+
if (existsSync(filePath)) {
|
|
318
|
+
try {
|
|
319
|
+
const content = readFileSync(filePath, "utf-8");
|
|
320
|
+
const body = stripFrontmatter(content).trim();
|
|
321
|
+
const baseDir = dirname(filePath);
|
|
322
|
+
return formatPiSkillBlock(
|
|
323
|
+
skillName,
|
|
324
|
+
filePath,
|
|
325
|
+
`References are relative to ${baseDir}.\n\n${body}`,
|
|
326
|
+
);
|
|
327
|
+
} catch {
|
|
328
|
+
// fall through
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const dirs = [join(getAgentDir(), "skills"), join(cwd, ".pi", "skills")];
|
|
333
|
+
for (const dir of dirs) {
|
|
334
|
+
for (const candidate of [
|
|
335
|
+
join(dir, skillName, "SKILL.md"),
|
|
336
|
+
join(dir, `${skillName}.md`),
|
|
337
|
+
]) {
|
|
338
|
+
try {
|
|
339
|
+
const content = readFileSync(candidate, "utf-8");
|
|
340
|
+
const body = stripFrontmatter(content).trim();
|
|
341
|
+
return formatPiSkillBlock(
|
|
342
|
+
skillName,
|
|
343
|
+
candidate,
|
|
344
|
+
`References are relative to ${dirname(candidate)}.\n\n${body}`,
|
|
345
|
+
);
|
|
346
|
+
} catch {
|
|
347
|
+
// continue
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
}
|