@zaganjade/pi-multi-skill 1.3.1 → 1.3.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 +34 -8
- package/package.json +4 -1
- package/skill-bundles.example.json +17 -17
- package/src/bmad-auto.ts +99 -99
- package/src/build.ts +395 -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,395 @@
|
|
|
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
|
-
return
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function
|
|
116
|
-
skill
|
|
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
|
-
if (
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Closing tag for INNER per-skill blocks inside a multi-skill payload.
|
|
13
|
+
*
|
|
14
|
+
* MUST NOT be `</skill>`. Pi's display parser (`parseSkillBlock`) uses a
|
|
15
|
+
* non-greedy regex that cannot distinguish a nested `</skill>` from the outer
|
|
16
|
+
* envelope's own closing tag. With nested `</skill>`, the parser split at the
|
|
17
|
+
* last inner `</skill>` and dumped every trailing section (`<user_query>`,
|
|
18
|
+
* `</manually_attached_skills>`, the outer `</skill>`) into the rendered
|
|
19
|
+
* `userMessage` — appearing as raw tags in the chat. Using a non-colliding
|
|
20
|
+
* closer keeps the outer envelope parseable so those sections stay inside the
|
|
21
|
+
* collapsed content.
|
|
22
|
+
*
|
|
23
|
+
* The opening tag stays `<skill name="…" location="…">` so pi-usage can still
|
|
24
|
+
* attribute each skill individually via `/<skill\s+name="([^"]+)"/g`; pi-usage
|
|
25
|
+
* only reads openings, so a non-colliding closer is safe. `</skill-block>` is
|
|
26
|
+
* chosen because it contains neither `</skill>` nor `<skill ` (with a space),
|
|
27
|
+
* so it cannot match either parser's regex.
|
|
28
|
+
*/
|
|
29
|
+
const INNER_SKILL_CLOSE = "</skill-block>";
|
|
30
|
+
const SKILL_CHECK_MARKERS = [
|
|
31
|
+
"If you think there is even a 1% chance a skill might apply",
|
|
32
|
+
"Invoke relevant or requested skills BEFORE any response",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** Pi TUI collapses user messages that match this envelope (see pi-coding-agent parseSkillBlock). */
|
|
36
|
+
const PI_SKILL_BLOCK_RE =
|
|
37
|
+
/^<skill name="([^"]+)" location="([^"]+)">\n([\s\S]*?)\n<\/skill>(?:\n\n([\s\S]+))?$/;
|
|
38
|
+
|
|
39
|
+
function getAgentDir(): string {
|
|
40
|
+
return join(homedir(), ".pi", "agent");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function escapeXml(text: string): string {
|
|
44
|
+
return text
|
|
45
|
+
.replace(/&/g, "&")
|
|
46
|
+
.replace(/"/g, """)
|
|
47
|
+
.replace(/</g, "<");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Wrap content in Pi's native skill block so the TUI shows `[skill] name`. */
|
|
51
|
+
export function formatPiSkillBlock(
|
|
52
|
+
name: string,
|
|
53
|
+
location: string,
|
|
54
|
+
content: string,
|
|
55
|
+
): string {
|
|
56
|
+
return `<skill name="${escapeXml(name)}" location="${escapeXml(location)}">\n${content}\n</skill>`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Wrap a single skill's content for inclusion INSIDE a multi-skill payload.
|
|
61
|
+
*
|
|
62
|
+
* Uses a non-colliding closer (see INNER_SKILL_CLOSE) so the outer native skill
|
|
63
|
+
* block parses cleanly with no tag leakage. The opening stays
|
|
64
|
+
* `<skill name="…">` to preserve pi-usage per-skill attribution.
|
|
65
|
+
*/
|
|
66
|
+
function formatInnerSkillItem(
|
|
67
|
+
name: string,
|
|
68
|
+
location: string,
|
|
69
|
+
content: string,
|
|
70
|
+
): string {
|
|
71
|
+
return `<skill name="${escapeXml(name)}" location="${escapeXml(location)}">\n${content}\n${INNER_SKILL_CLOSE}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isPiSkillBlock(text: string): boolean {
|
|
75
|
+
return PI_SKILL_BLOCK_RE.test(text);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatCollapsedSkillName(skills: EnrichedSkillInfo[]): string {
|
|
79
|
+
const names = skills.map((s) => s.name);
|
|
80
|
+
if (names.length === 1) return names[0];
|
|
81
|
+
if (names.length <= 3) return names.join(", ");
|
|
82
|
+
return `${names.slice(0, 2).join(", ")} +${names.length - 2} more`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readSkillFile(skill: EnrichedSkillInfo, cwd: string): string | null {
|
|
86
|
+
try {
|
|
87
|
+
return readFileSync(skill.filePath, "utf-8");
|
|
88
|
+
} catch {
|
|
89
|
+
// fall through
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const dirs = [join(getAgentDir(), "skills"), join(cwd, ".pi", "skills")];
|
|
93
|
+
for (const dir of dirs) {
|
|
94
|
+
for (const candidate of [
|
|
95
|
+
join(dir, skill.name, "SKILL.md"),
|
|
96
|
+
join(dir, `${skill.name}.md`),
|
|
97
|
+
]) {
|
|
98
|
+
try {
|
|
99
|
+
return readFileSync(candidate, "utf-8");
|
|
100
|
+
} catch {
|
|
101
|
+
// continue
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function extractCommandsSection(body: string): string {
|
|
109
|
+
const match = body.match(
|
|
110
|
+
/## Available Commands[\s\S]*?(?=\n## |\n# |$)/i,
|
|
111
|
+
);
|
|
112
|
+
return match ? match[0].trim() : "";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatMetaBody(skill: EnrichedSkillInfo): string {
|
|
116
|
+
const commands = skill.metadata.commands.length
|
|
117
|
+
? `\n\nCommands: ${skill.metadata.commands.join(", ")}`
|
|
118
|
+
: "";
|
|
119
|
+
const commandsSection = extractCommandsSection(skill.body);
|
|
120
|
+
const sectionText = commandsSection ? `\n\n${commandsSection}` : "";
|
|
121
|
+
|
|
122
|
+
return `(load mode: meta)\n\n${skill.metadata.description}${commands}${sectionText}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function formatLazyBody(skill: EnrichedSkillInfo, body: string): string {
|
|
126
|
+
const baseDir = dirname(skill.filePath);
|
|
127
|
+
const intro = body
|
|
128
|
+
.split("\n")
|
|
129
|
+
.filter((l) => l.trim() && !l.startsWith("#"))
|
|
130
|
+
.slice(0, 8)
|
|
131
|
+
.join("\n");
|
|
132
|
+
|
|
133
|
+
return [
|
|
134
|
+
`(load mode: lazy)`,
|
|
135
|
+
`References are relative to ${baseDir}. Load reference files with \`read\` only when the active workflow step requires them.`,
|
|
136
|
+
"",
|
|
137
|
+
intro,
|
|
138
|
+
extractCommandsSection(body),
|
|
139
|
+
]
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
.join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function formatFullBody(skill: EnrichedSkillInfo, body: string): string {
|
|
145
|
+
const baseDir = dirname(skill.filePath);
|
|
146
|
+
return `References are relative to ${baseDir}.\n\n${body}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function dedupeBody(name: string, body: string, seen: Set<string>): string {
|
|
150
|
+
let result = body;
|
|
151
|
+
|
|
152
|
+
if (body.includes(SUBAGENT_STOP)) {
|
|
153
|
+
if (seen.has(SUBAGENT_STOP)) {
|
|
154
|
+
result = result.replace(
|
|
155
|
+
new RegExp(`${SUBAGENT_STOP}[\\s\\S]*?${SUBAGENT_STOP}`, "g"),
|
|
156
|
+
"",
|
|
157
|
+
);
|
|
158
|
+
} else {
|
|
159
|
+
seen.add(SUBAGENT_STOP);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (name !== "using-superpowers") {
|
|
164
|
+
for (const marker of SKILL_CHECK_MARKERS) {
|
|
165
|
+
if (result.includes(marker) && seen.has(marker)) {
|
|
166
|
+
result = result.replace(
|
|
167
|
+
new RegExp(`## Instruction Priority[\\s\\S]*?(?=\\n## |$)`, "i"),
|
|
168
|
+
"",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
if (result.includes(marker)) seen.add(marker);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return result.trim();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function renderSkillBlock(
|
|
179
|
+
skill: EnrichedSkillInfo,
|
|
180
|
+
cwd: string,
|
|
181
|
+
mode: LoadMode,
|
|
182
|
+
seenDedup: Set<string>,
|
|
183
|
+
): string | null {
|
|
184
|
+
const effectiveMode = skill.metadata.tokenBudget ?? mode;
|
|
185
|
+
const raw = readSkillFile(skill, cwd);
|
|
186
|
+
if (!raw) return null;
|
|
187
|
+
|
|
188
|
+
const body = dedupeBody(skill.name, stripFrontmatter(raw).trim(), seenDedup);
|
|
189
|
+
let content: string;
|
|
190
|
+
switch (effectiveMode) {
|
|
191
|
+
case "meta":
|
|
192
|
+
content = formatMetaBody(skill);
|
|
193
|
+
break;
|
|
194
|
+
case "lazy":
|
|
195
|
+
content = formatLazyBody(skill, body);
|
|
196
|
+
break;
|
|
197
|
+
default:
|
|
198
|
+
content = formatFullBody(skill, body);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return formatInnerSkillItem(skill.name, skill.filePath, content);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function buildAgentPayload(
|
|
205
|
+
skills: EnrichedSkillInfo[],
|
|
206
|
+
expandedBlocks: string[],
|
|
207
|
+
options: BuildOptions,
|
|
208
|
+
notFound: string[],
|
|
209
|
+
skippedDuplicates: string[],
|
|
210
|
+
): string {
|
|
211
|
+
const skillNames = skills.map((s) => s.name).join(", ");
|
|
212
|
+
const bundleAttr =
|
|
213
|
+
options.bundles && options.bundles.length > 0
|
|
214
|
+
? ` bundles="${options.bundles.map((b) => `@${b}`).join(",")}"`
|
|
215
|
+
: "";
|
|
216
|
+
const parts: string[] = [
|
|
217
|
+
`<manually_attached_skills count="${skills.length}"${bundleAttr}>`,
|
|
218
|
+
"The user activated multiple skills. Follow ALL of them before responding.",
|
|
219
|
+
"Priority: process skills → planning → implementation. User instructions override conflicts.",
|
|
220
|
+
`Skills: ${skillNames}`,
|
|
221
|
+
`Load mode: ${options.mode}`,
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
if (options.parallel) {
|
|
225
|
+
const tasks =
|
|
226
|
+
options.parallelTasks && options.parallelTasks.length > 0
|
|
227
|
+
? options.parallelTasks
|
|
228
|
+
: options.instructions
|
|
229
|
+
? [options.instructions]
|
|
230
|
+
: [];
|
|
231
|
+
parts.push(
|
|
232
|
+
buildParallelDispatchBlock({
|
|
233
|
+
tasks,
|
|
234
|
+
subagentAvailable: options.subagentAvailable ?? false,
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
parts.push("");
|
|
240
|
+
parts.push(...expandedBlocks);
|
|
241
|
+
|
|
242
|
+
if (options.bmadStatusBlock) {
|
|
243
|
+
parts.push("");
|
|
244
|
+
parts.push(options.bmadStatusBlock);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (options.embeddedCommand) {
|
|
248
|
+
parts.push("");
|
|
249
|
+
parts.push(
|
|
250
|
+
`<embedded_command>${options.embeddedCommand}</embedded_command>`,
|
|
251
|
+
);
|
|
252
|
+
parts.push(
|
|
253
|
+
"Execute the embedded command workflow as part of fulfilling the user request.",
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// NOTE: options.instructions is intentionally NOT added inside the payload.
|
|
258
|
+
// It is appended after the outer </skill> envelope in buildCombinedMessage
|
|
259
|
+
// so Pi renders it as the visible user message (the `userMessage` tail of
|
|
260
|
+
// parseSkillBlock) while the skill content stays collapsed. Wrapping it in
|
|
261
|
+
// <user_query> tags here would leak those tags into the rendered message.
|
|
262
|
+
|
|
263
|
+
if (notFound.length > 0) {
|
|
264
|
+
parts.push("");
|
|
265
|
+
parts.push(`> ⚠️ Skills not found: ${notFound.join(", ")}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (options.conflictWarnings?.length) {
|
|
269
|
+
parts.push("");
|
|
270
|
+
parts.push(`> ⚠️ Conflicts: ${options.conflictWarnings.join("; ")}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (skippedDuplicates.length > 0) {
|
|
274
|
+
parts.push("");
|
|
275
|
+
parts.push(`> ℹ️ Deduplicated: ${skippedDuplicates.join("; ")}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
parts.push("</manually_attached_skills>");
|
|
279
|
+
return parts.join("\n");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function shouldWrapForDisplay(
|
|
283
|
+
skills: EnrichedSkillInfo[],
|
|
284
|
+
options: BuildOptions,
|
|
285
|
+
): boolean {
|
|
286
|
+
if (skills.length > 1) return true;
|
|
287
|
+
if (options.parallel || options.bmadStatusBlock || options.embeddedCommand) {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
if (options.instructions) return true;
|
|
291
|
+
if (options.bundles && options.bundles.length > 0) return true;
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function buildCombinedMessage(
|
|
296
|
+
skills: EnrichedSkillInfo[],
|
|
297
|
+
cwd: string,
|
|
298
|
+
options: BuildOptions,
|
|
299
|
+
): { message: string; notFound: string[]; skippedDuplicates: string[] } {
|
|
300
|
+
const expandedBlocks: string[] = [];
|
|
301
|
+
const notFound: string[] = [];
|
|
302
|
+
const seenDedup = new Set<string>();
|
|
303
|
+
const skippedDuplicates: string[] = [];
|
|
304
|
+
|
|
305
|
+
for (const skill of skills) {
|
|
306
|
+
const before = seenDedup.size;
|
|
307
|
+
const block = renderSkillBlock(skill, cwd, options.mode, seenDedup);
|
|
308
|
+
if (!block) {
|
|
309
|
+
notFound.push(skill.name);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (seenDedup.size > before && before > 0) {
|
|
313
|
+
skippedDuplicates.push(`${skill.name} (deduplicated sections)`);
|
|
314
|
+
}
|
|
315
|
+
expandedBlocks.push(block);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const payload = buildAgentPayload(
|
|
319
|
+
skills,
|
|
320
|
+
expandedBlocks,
|
|
321
|
+
options,
|
|
322
|
+
notFound,
|
|
323
|
+
skippedDuplicates,
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
let message: string;
|
|
327
|
+
if (expandedBlocks.length === 0) {
|
|
328
|
+
message = payload;
|
|
329
|
+
} else if (shouldWrapForDisplay(skills, options)) {
|
|
330
|
+
message = formatPiSkillBlock(
|
|
331
|
+
formatCollapsedSkillName(skills),
|
|
332
|
+
skills.length === 1 ? skills[0].filePath : MULTI_SKILL_LOCATION,
|
|
333
|
+
payload,
|
|
334
|
+
);
|
|
335
|
+
} else {
|
|
336
|
+
message = expandedBlocks[0];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Surface the user's free-text instructions as Pi's `userMessage` tail
|
|
340
|
+
// (the optional text after `</skill>\n\n`). Pi renders the skill block
|
|
341
|
+
// collapsed as `[skill] a, b (ctrl+o to expand)` and this tail as a normal
|
|
342
|
+
// visible user message below it — which is the desired display. Plain text
|
|
343
|
+
// only: wrapping tags here would render literally.
|
|
344
|
+
if (options.instructions && expandedBlocks.length > 0) {
|
|
345
|
+
message = `${message}\n\n${options.instructions}`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
message,
|
|
350
|
+
notFound,
|
|
351
|
+
skippedDuplicates,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function resolveAndReadLegacySkill(
|
|
356
|
+
skillName: string,
|
|
357
|
+
filePath: string,
|
|
358
|
+
cwd: string,
|
|
359
|
+
): string | null {
|
|
360
|
+
if (existsSync(filePath)) {
|
|
361
|
+
try {
|
|
362
|
+
const content = readFileSync(filePath, "utf-8");
|
|
363
|
+
const body = stripFrontmatter(content).trim();
|
|
364
|
+
const baseDir = dirname(filePath);
|
|
365
|
+
return formatPiSkillBlock(
|
|
366
|
+
skillName,
|
|
367
|
+
filePath,
|
|
368
|
+
`References are relative to ${baseDir}.\n\n${body}`,
|
|
369
|
+
);
|
|
370
|
+
} catch {
|
|
371
|
+
// fall through
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const dirs = [join(getAgentDir(), "skills"), join(cwd, ".pi", "skills")];
|
|
376
|
+
for (const dir of dirs) {
|
|
377
|
+
for (const candidate of [
|
|
378
|
+
join(dir, skillName, "SKILL.md"),
|
|
379
|
+
join(dir, `${skillName}.md`),
|
|
380
|
+
]) {
|
|
381
|
+
try {
|
|
382
|
+
const content = readFileSync(candidate, "utf-8");
|
|
383
|
+
const body = stripFrontmatter(content).trim();
|
|
384
|
+
return formatPiSkillBlock(
|
|
385
|
+
skillName,
|
|
386
|
+
candidate,
|
|
387
|
+
`References are relative to ${dirname(candidate)}.\n\n${body}`,
|
|
388
|
+
);
|
|
389
|
+
} catch {
|
|
390
|
+
// continue
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|