@zaganjade/pi-multi-skill 1.0.0 → 1.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 +453 -17
- package/package.json +9 -3
- package/skill-bundles.example.json +17 -0
- package/skill-bundles.example.yaml +9 -0
- package/src/bmad-auto.ts +99 -0
- package/src/bmad-status.ts +125 -0
- package/src/build.ts +270 -0
- package/src/bundle-status.ts +178 -0
- package/src/bundles.ts +138 -0
- package/src/completions.ts +184 -0
- package/src/conflicts.ts +26 -0
- package/src/discover.ts +161 -0
- package/src/index.ts +287 -345
- package/src/metadata.ts +170 -0
- package/src/parse-args.ts +80 -0
- package/src/registry.ts +46 -0
- package/src/stats.ts +164 -0
- package/src/subagents.ts +75 -0
- package/src/suggestions.ts +70 -0
- package/src/types.ts +64 -0
- package/src/yaml-bundles.ts +97 -0
package/src/index.ts
CHANGED
|
@@ -1,395 +1,332 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionCommandContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
6
|
+
import {
|
|
7
|
+
bundleOrderHint,
|
|
8
|
+
DEFAULT_BUNDLES,
|
|
9
|
+
expandSkillNames,
|
|
10
|
+
loadBundles,
|
|
11
|
+
} from "./bundles.ts";
|
|
12
|
+
import {
|
|
13
|
+
assessAllBundles,
|
|
14
|
+
formatBundleFailureGuide,
|
|
15
|
+
formatBundleHelpLine,
|
|
16
|
+
formatMissingSkillHints,
|
|
17
|
+
formatSetupReport,
|
|
18
|
+
} from "./bundle-status.ts";
|
|
19
|
+
import { bmadAutoHint, resolveBmadAutoSkills } from "./bmad-auto.ts";
|
|
20
|
+
import { buildBmadStatusBlock, shouldInjectBmadStatus } from "./bmad-status.ts";
|
|
21
|
+
import { buildCombinedMessage, resolveAndReadLegacySkill } from "./build.ts";
|
|
22
|
+
import { getSkillsArgumentCompletions } from "./completions.ts";
|
|
23
|
+
import { resolveSkillConflicts } from "./conflicts.ts";
|
|
24
|
+
import { discoverAllSkills, resolveSkillByName } from "./discover.ts";
|
|
25
|
+
import { enrichSkill, sortSkills } from "./metadata.ts";
|
|
26
|
+
import { parseSkillsArgs } from "./parse-args.ts";
|
|
27
|
+
import { rebuildSkillIndex } from "./registry.ts";
|
|
28
|
+
import { formatStatsReport, recordActivation, replayLastArgs, formatLastActivationHint } from "./stats.ts";
|
|
29
|
+
import { formatSuggestionHint, suggestSkillBundle } from "./suggestions.ts";
|
|
30
|
+
import { hasSubagentTool } from "./subagents.ts";
|
|
31
|
+
import type { EnrichedSkillInfo, LoadMode, SkillBundle, SkillInfo, SkillOrder } from "./types.ts";
|
|
6
32
|
|
|
7
33
|
/**
|
|
8
34
|
* Multi-Skill Loader Extension for Pi
|
|
9
|
-
*
|
|
10
|
-
* Enables loading multiple skills at once via the /skills command.
|
|
11
|
-
* Appears in the slash autocomplete menu alongside built-in commands,
|
|
12
|
-
* with full skill descriptions shown just like /skill: commands.
|
|
13
|
-
*
|
|
14
|
-
* Uses pi.getCommands() to discover ALL skills from every source:
|
|
15
|
-
* user-level, project-level, npm packages, git packages, etc.
|
|
16
|
-
*
|
|
17
|
-
* Usage:
|
|
18
|
-
* /skills frontend-design,motion-design Create an animated landing page
|
|
19
|
-
* /skills → shows help + available skills
|
|
20
|
-
*
|
|
21
|
-
* Also handles legacy formats via the input event:
|
|
22
|
-
* /skills:frontend-design,motion-design [args] (colon-separated)
|
|
23
|
-
* /skill:frontend-design+motion-design [args] (plus-separated)
|
|
24
35
|
*/
|
|
25
36
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return match ? normalized.slice(match[0].length) : normalized;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Truncate a description to a single readable line for autocomplete display. */
|
|
45
|
-
function truncateDescription(desc: string, maxLen = 120): string {
|
|
46
|
-
if (!desc) return "";
|
|
47
|
-
|
|
48
|
-
// Take first non-empty line
|
|
49
|
-
const lines = desc
|
|
50
|
-
.split("\n")
|
|
51
|
-
.map((l) => l.replace(/^>\s*/, "").trim())
|
|
52
|
-
.filter((l) => l.length > 0);
|
|
53
|
-
|
|
54
|
-
if (lines.length === 0) return "";
|
|
55
|
-
|
|
56
|
-
let text = lines[0];
|
|
57
|
-
if (lines.length > 1 && text.length < 40) {
|
|
58
|
-
text = `${text} ${lines[1]}`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (text.length > maxLen) {
|
|
62
|
-
text = `${text.slice(0, maxLen - 1)}…`;
|
|
37
|
+
function resolveSelectedSkills(
|
|
38
|
+
names: string[],
|
|
39
|
+
available: SkillInfo[],
|
|
40
|
+
order: SkillOrder = "process-first",
|
|
41
|
+
): EnrichedSkillInfo[] {
|
|
42
|
+
const nameToSkill = new Map(available.map((s) => [s.name, s]));
|
|
43
|
+
const selected: SkillInfo[] = [];
|
|
44
|
+
|
|
45
|
+
for (const name of names) {
|
|
46
|
+
const skill =
|
|
47
|
+
nameToSkill.get(name) ?? resolveSkillByName(name, nameToSkill);
|
|
48
|
+
if (skill) {
|
|
49
|
+
nameToSkill.set(skill.name, skill);
|
|
50
|
+
selected.push(skill);
|
|
51
|
+
}
|
|
63
52
|
}
|
|
64
53
|
|
|
65
|
-
return
|
|
54
|
+
return sortSkills(selected.map(enrichSkill), order);
|
|
66
55
|
}
|
|
67
56
|
|
|
68
|
-
function
|
|
69
|
-
|
|
57
|
+
function resolveSelectedSkillsWithConflicts(
|
|
58
|
+
names: string[],
|
|
59
|
+
available: SkillInfo[],
|
|
60
|
+
order: SkillOrder = "process-first",
|
|
61
|
+
): { skills: EnrichedSkillInfo[]; warnings: string[] } {
|
|
62
|
+
const sorted = resolveSelectedSkills(names, available, order);
|
|
63
|
+
const resolved = resolveSkillConflicts(sorted);
|
|
64
|
+
return { skills: resolved.skills, warnings: resolved.warnings };
|
|
70
65
|
}
|
|
71
66
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
skills
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
67
|
+
function formatHelp(available: SkillInfo[], bundles: Map<string, SkillBundle>): string {
|
|
68
|
+
const skillLines = available
|
|
69
|
+
.map((s) =>
|
|
70
|
+
s.description ? ` • ${s.name} — ${s.description}` : ` • ${s.name}`,
|
|
71
|
+
)
|
|
72
|
+
.join("\n");
|
|
73
|
+
|
|
74
|
+
const bundleStatuses = assessAllBundles(bundles, available);
|
|
75
|
+
const bundleLines = bundleStatuses.map(formatBundleHelpLine).join("\n");
|
|
76
|
+
const readyCount = bundleStatuses.filter((s) => s.ready).length;
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
`Usage:\n` +
|
|
80
|
+
` /skills skill1,skill2 [instructions]\n` +
|
|
81
|
+
` /skills @bundle [--meta|--lazy|--full] [instructions]\n` +
|
|
82
|
+
` /skills bmad-master /workflow-status\n` +
|
|
83
|
+
` /skills @bmad-planning --auto\n` +
|
|
84
|
+
` /skills @cc-feature --parallel Task A | Task B | Task C\n\n` +
|
|
85
|
+
`Flags: --meta · --lazy · --full · --auto · --parallel\n` +
|
|
86
|
+
`Setup: /skills-setup · Stats: /skills-stats · Repeat: /skills-last\n\n` +
|
|
87
|
+
`Bundles (${readyCount}/${bundles.size} usable on this machine):\n${bundleLines}\n\n` +
|
|
88
|
+
`Skills (${available.length}):\n${skillLines}\n\n` +
|
|
89
|
+
`No BMAD/Superpowers? Use /skills-setup or create ~/.pi/agent/skill-bundles.json`
|
|
90
|
+
);
|
|
100
91
|
}
|
|
101
92
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const skillFile = join(dir, entry.name, "SKILL.md");
|
|
122
|
-
if (existsSync(skillFile) && !knownNames.has(entry.name)) {
|
|
123
|
-
skills.push({
|
|
124
|
-
name: entry.name,
|
|
125
|
-
description: "",
|
|
126
|
-
filePath: skillFile,
|
|
127
|
-
baseDir: join(dir, entry.name),
|
|
128
|
-
});
|
|
129
|
-
knownNames.add(entry.name);
|
|
130
|
-
}
|
|
131
|
-
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
132
|
-
const name = entry.name.replace(/\.md$/, "");
|
|
133
|
-
if (!knownNames.has(name)) {
|
|
134
|
-
const skillFile = join(dir, entry.name);
|
|
135
|
-
skills.push({
|
|
136
|
-
name,
|
|
137
|
-
description: "",
|
|
138
|
-
filePath: skillFile,
|
|
139
|
-
baseDir: dir,
|
|
140
|
-
});
|
|
141
|
-
knownNames.add(name);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
} catch {
|
|
146
|
-
// Skip unreadable directories
|
|
147
|
-
}
|
|
93
|
+
function processAndSend(
|
|
94
|
+
pi: ExtensionAPI,
|
|
95
|
+
ctx: ExtensionCommandContext,
|
|
96
|
+
rawArgs: string,
|
|
97
|
+
getSkills: (cwd: string) => SkillInfo[],
|
|
98
|
+
): void {
|
|
99
|
+
const bundles = loadBundles(ctx.cwd);
|
|
100
|
+
const parsed = parseSkillsArgs(rawArgs);
|
|
101
|
+
let skillNames = [...parsed.skillNames];
|
|
102
|
+
let mode: LoadMode = parsed.mode;
|
|
103
|
+
|
|
104
|
+
if (parsed.auto) {
|
|
105
|
+
skillNames = resolveBmadAutoSkills(ctx.cwd);
|
|
106
|
+
const hint = bmadAutoHint(ctx.cwd);
|
|
107
|
+
if (hint) ctx.ui.notify(hint, "info");
|
|
108
|
+
} else {
|
|
109
|
+
const expanded = expandSkillNames(skillNames, bundles);
|
|
110
|
+
skillNames = expanded.skills;
|
|
111
|
+
if (mode === "full" && expanded.modeHint) mode = expanded.modeHint;
|
|
148
112
|
}
|
|
149
113
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
// Try all possible skill locations
|
|
155
|
-
const dirs = [join(getAgentDir(), "skills"), join(cwd, ".pi", "skills")];
|
|
156
|
-
|
|
157
|
-
for (const dir of dirs) {
|
|
158
|
-
// Directory-based: <dir>/<name>/SKILL.md
|
|
159
|
-
const skillFile = join(dir, skillName, "SKILL.md");
|
|
160
|
-
try {
|
|
161
|
-
const content = readFileSync(skillFile, "utf-8");
|
|
162
|
-
const body = stripFrontmatter(content).trim();
|
|
163
|
-
return `<skill name="${skillName}" location="${skillFile}">\nReferences are relative to ${dirname(skillFile)}.\n\n${body}\n</skill>`;
|
|
164
|
-
} catch {
|
|
165
|
-
// not found
|
|
166
|
-
}
|
|
114
|
+
if (skillNames.length === 0) {
|
|
115
|
+
ctx.ui.notify("No skill names provided.", "error");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
167
118
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
119
|
+
const bundleNames = parsed.skillNames
|
|
120
|
+
.filter((n) => n.startsWith("@"))
|
|
121
|
+
.map((n) => n.slice(1));
|
|
122
|
+
|
|
123
|
+
const allSkills = getSkills(ctx.cwd);
|
|
124
|
+
const order =
|
|
125
|
+
bundleOrderHint(parsed.skillNames, bundles) ?? "process-first";
|
|
126
|
+
const { skills: selected, warnings: conflictWarnings } =
|
|
127
|
+
resolveSelectedSkillsWithConflicts(skillNames, allSkills, order);
|
|
128
|
+
|
|
129
|
+
if (selected.length === 0) {
|
|
130
|
+
if (bundleNames.length > 0) {
|
|
131
|
+
ctx.ui.notify(
|
|
132
|
+
formatBundleFailureGuide(bundleNames, bundles, allSkills),
|
|
133
|
+
"error",
|
|
134
|
+
);
|
|
135
|
+
} else {
|
|
136
|
+
ctx.ui.notify(
|
|
137
|
+
`No skills found for: ${skillNames.join(", ")}\n` +
|
|
138
|
+
`Install skills under ~/.pi/agent/skills, ~/.claude/skills, or pi settings "skills" paths.\n` +
|
|
139
|
+
`Run /skills-setup for bundle install guide.`,
|
|
140
|
+
"error",
|
|
141
|
+
);
|
|
176
142
|
}
|
|
143
|
+
return;
|
|
177
144
|
}
|
|
178
145
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const content = readFileSync(filePath, "utf-8");
|
|
192
|
-
const body = stripFrontmatter(content).trim();
|
|
193
|
-
const baseDir = dirname(filePath);
|
|
194
|
-
return `<skill name="${skillName}" location="${filePath}">\nReferences are relative to ${baseDir}.\n\n${body}\n</skill>`;
|
|
195
|
-
} catch {
|
|
196
|
-
return null;
|
|
146
|
+
const missing = skillNames.filter(
|
|
147
|
+
(name) => !selected.some((s) => s.name === name),
|
|
148
|
+
);
|
|
149
|
+
if (missing.length > 0) {
|
|
150
|
+
const hint =
|
|
151
|
+
bundleNames.length > 0
|
|
152
|
+
? `Skills not found (partial bundle load):\n${formatMissingSkillHints(missing)}\nRun /skills-setup for install guide.`
|
|
153
|
+
: `Skills not found: ${missing.join(", ")}`;
|
|
154
|
+
ctx.ui.notify(hint, "info");
|
|
155
|
+
}
|
|
156
|
+
if (conflictWarnings.length > 0) {
|
|
157
|
+
ctx.ui.notify(conflictWarnings.join("\n"), "info");
|
|
197
158
|
}
|
|
198
|
-
}
|
|
199
159
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const expandedBlocks: string[] = [];
|
|
207
|
-
const notFound: string[] = [];
|
|
208
|
-
|
|
209
|
-
for (const skill of selectedSkills) {
|
|
210
|
-
// Try reading by the known filePath first (works for package skills)
|
|
211
|
-
let content = resolveAndReadSkillByPath(skill.name, skill.filePath);
|
|
212
|
-
|
|
213
|
-
// Fallback to filesystem scan for user/project skills
|
|
214
|
-
if (!content) {
|
|
215
|
-
content = resolveAndReadSkill(skill.name, cwd);
|
|
216
|
-
}
|
|
160
|
+
if (parsed.embeddedCommand) {
|
|
161
|
+
ctx.ui.notify(
|
|
162
|
+
`Loading ${selected.map((s) => s.name).join(", ")} → ${parsed.embeddedCommand}`,
|
|
163
|
+
"info",
|
|
164
|
+
);
|
|
165
|
+
}
|
|
217
166
|
|
|
218
|
-
|
|
219
|
-
|
|
167
|
+
const subagentAvailable = hasSubagentTool(pi);
|
|
168
|
+
if (parsed.parallel && !subagentAvailable) {
|
|
169
|
+
ctx.ui.notify(
|
|
170
|
+
"Parallel mode: pi-subagents not detected — tasks will run sequentially. Install: pi install npm:pi-subagents",
|
|
171
|
+
"info",
|
|
172
|
+
);
|
|
220
173
|
}
|
|
221
174
|
|
|
222
|
-
|
|
175
|
+
const bmadStatusBlock = shouldInjectBmadStatus({
|
|
176
|
+
embeddedCommand: parsed.embeddedCommand,
|
|
177
|
+
auto: parsed.auto,
|
|
178
|
+
})
|
|
179
|
+
? buildBmadStatusBlock(ctx.cwd)
|
|
180
|
+
: null;
|
|
181
|
+
|
|
182
|
+
const { message } = buildCombinedMessage(selected, ctx.cwd, {
|
|
183
|
+
mode,
|
|
184
|
+
bundles: bundleNames,
|
|
185
|
+
bmadStatusBlock: bmadStatusBlock ?? undefined,
|
|
186
|
+
parallel: parsed.parallel,
|
|
187
|
+
parallelTasks: parsed.parallelTasks,
|
|
188
|
+
subagentAvailable,
|
|
189
|
+
embeddedCommand: parsed.embeddedCommand,
|
|
190
|
+
instructions: parsed.instructions || undefined,
|
|
191
|
+
conflictWarnings,
|
|
192
|
+
});
|
|
223
193
|
|
|
224
|
-
if (
|
|
225
|
-
|
|
194
|
+
if (!message.includes("<skill ")) {
|
|
195
|
+
ctx.ui.notify(`Could not read skill files for: ${skillNames.join(", ")}`, "error");
|
|
196
|
+
return;
|
|
226
197
|
}
|
|
227
198
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
199
|
+
recordActivation({
|
|
200
|
+
mode,
|
|
201
|
+
skillNames: selected.map((s) => s.name),
|
|
202
|
+
bundles: bundleNames,
|
|
203
|
+
parallel: parsed.parallel,
|
|
204
|
+
skillCount: selected.length,
|
|
205
|
+
rawArgs,
|
|
206
|
+
});
|
|
231
207
|
|
|
232
|
-
|
|
208
|
+
pi.sendUserMessage(message);
|
|
233
209
|
}
|
|
234
210
|
|
|
235
|
-
// ─── Extension ──────────────────────────────────────────────────────────────
|
|
236
|
-
|
|
237
211
|
export default function (pi: ExtensionAPI) {
|
|
238
|
-
// Cache skills list for the session (refreshed on reload)
|
|
239
212
|
let cachedSkills: SkillInfo[] | null = null;
|
|
240
213
|
|
|
241
|
-
function getSkills(cwd
|
|
214
|
+
function getSkills(cwd: string): SkillInfo[] {
|
|
242
215
|
if (!cachedSkills) {
|
|
243
|
-
|
|
244
|
-
const piSkills = discoverSkillsFromPi(pi);
|
|
245
|
-
const knownNames = new Set(piSkills.map((s) => s.name));
|
|
246
|
-
|
|
247
|
-
// Secondary: filesystem fallback for anything missed
|
|
248
|
-
const fsSkills = cwd ? discoverSkillsFromFilesystem(cwd, knownNames) : [];
|
|
249
|
-
|
|
250
|
-
cachedSkills = [...piSkills, ...fsSkills].sort((a, b) =>
|
|
251
|
-
a.name.localeCompare(b.name),
|
|
252
|
-
);
|
|
216
|
+
cachedSkills = discoverAllSkills(pi, cwd);
|
|
253
217
|
}
|
|
254
218
|
return cachedSkills;
|
|
255
219
|
}
|
|
256
220
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
221
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
222
|
+
try {
|
|
223
|
+
cachedSkills = null;
|
|
224
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
225
|
+
rebuildSkillIndex(getSkills(cwd));
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.error("[pi-multi-skill] session_start:", err);
|
|
228
|
+
}
|
|
260
229
|
});
|
|
261
230
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
231
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
232
|
+
try {
|
|
233
|
+
const sm = ctx?.sessionManager;
|
|
234
|
+
if (!sm) return;
|
|
235
|
+
|
|
236
|
+
for (const entry of [...sm.getBranch()].reverse()) {
|
|
237
|
+
if (entry.type !== "message") continue;
|
|
238
|
+
if (entry.message.role !== "user") continue;
|
|
239
|
+
const content = entry.message.content;
|
|
240
|
+
const text = typeof content === "string" ? content : "";
|
|
241
|
+
if (
|
|
242
|
+
text.includes("<manually_attached_skills") ||
|
|
243
|
+
text.includes("<skill ")
|
|
244
|
+
) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const suggestion = suggestSkillBundle(
|
|
248
|
+
text,
|
|
249
|
+
loadBundles(ctx.cwd),
|
|
250
|
+
getSkills(ctx.cwd),
|
|
251
|
+
);
|
|
252
|
+
if (suggestion) {
|
|
253
|
+
ctx.ui.notify(
|
|
254
|
+
formatSuggestionHint(suggestion.bundle, suggestion.reason),
|
|
255
|
+
"info",
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
290
259
|
}
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.error("[pi-multi-skill] turn_end:", err);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
291
264
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
description: s.description || undefined,
|
|
303
|
-
}));
|
|
304
|
-
},
|
|
265
|
+
pi.registerCommand("skills", {
|
|
266
|
+
description:
|
|
267
|
+
"Load multiple skills — /skills @bundle,skill1,skill2 [--meta|--lazy] [instructions]",
|
|
268
|
+
getArgumentCompletions: (prefix: string): AutocompleteItem[] | null =>
|
|
269
|
+
getSkillsArgumentCompletions(
|
|
270
|
+
prefix,
|
|
271
|
+
getSkills(process.cwd()),
|
|
272
|
+
loadBundles(process.cwd()),
|
|
273
|
+
pi,
|
|
274
|
+
),
|
|
305
275
|
handler: async (args, ctx) => {
|
|
306
276
|
const available = getSkills(ctx.cwd);
|
|
277
|
+
const bundles = loadBundles(ctx.cwd);
|
|
307
278
|
|
|
308
279
|
if (!args || args.trim().length === 0) {
|
|
309
|
-
|
|
310
|
-
const skillLines = available
|
|
311
|
-
.map((s) =>
|
|
312
|
-
s.description
|
|
313
|
-
? ` • ${s.name} — ${s.description}`
|
|
314
|
-
: ` • ${s.name}`,
|
|
315
|
-
)
|
|
316
|
-
.join("\n");
|
|
317
|
-
|
|
318
|
-
ctx.ui.notify(
|
|
319
|
-
`Usage: /skills skill1,skill2,... [additional instructions]\n` +
|
|
320
|
-
`Example: /skills frontend-design,motion-design Create an animated page\n\n` +
|
|
321
|
-
`Available skills (${available.length}):\n${skillLines}`,
|
|
322
|
-
"info",
|
|
323
|
-
);
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Parse: first token is comma-separated skill names, rest is instructions
|
|
328
|
-
const tokens = args.trim().match(/^(\S+)(?:\s+([\s\S]*))?$/);
|
|
329
|
-
if (!tokens) {
|
|
330
|
-
ctx.ui.notify(
|
|
331
|
-
"Invalid format. Usage: /skills skill1,skill2 [instructions]",
|
|
332
|
-
"error",
|
|
333
|
-
);
|
|
280
|
+
ctx.ui.notify(formatHelp(available, bundles), "info");
|
|
334
281
|
return;
|
|
335
282
|
}
|
|
336
283
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
.filter((s) => s.length > 0);
|
|
341
|
-
const instructions = tokens[2]?.trim() || "";
|
|
342
|
-
|
|
343
|
-
if (skillNames.length === 0) {
|
|
344
|
-
ctx.ui.notify("No skill names provided.", "error");
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
284
|
+
processAndSend(pi, ctx, args, getSkills);
|
|
285
|
+
},
|
|
286
|
+
});
|
|
347
287
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
288
|
+
pi.registerCommand("skills-stats", {
|
|
289
|
+
description: "Show multi-skill activation statistics",
|
|
290
|
+
handler: async (_args, ctx) => {
|
|
291
|
+
ctx.ui.notify(formatStatsReport(), "info");
|
|
292
|
+
},
|
|
293
|
+
});
|
|
353
294
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
);
|
|
295
|
+
pi.registerCommand("skills-setup", {
|
|
296
|
+
description: "Bundle prerequisites and install guide (BMAD, Superpowers, custom bundles)",
|
|
297
|
+
handler: async (_args, ctx) => {
|
|
298
|
+
const available = getSkills(ctx.cwd);
|
|
299
|
+
const bundles = loadBundles(ctx.cwd);
|
|
300
|
+
ctx.ui.notify(formatSetupReport(bundles, available), "info");
|
|
301
|
+
},
|
|
302
|
+
});
|
|
359
303
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
304
|
+
pi.registerCommand("skills-last", {
|
|
305
|
+
description: "Repeat the last /skills activation (optional: --meta|--lazy|--full|--parallel)",
|
|
306
|
+
handler: async (args, ctx) => {
|
|
307
|
+
const replay = replayLastArgs(args?.trim() ?? "");
|
|
308
|
+
if (!replay) {
|
|
309
|
+
ctx.ui.notify(
|
|
310
|
+
"No recent /skills activation to replay.\nRun /skills first, then /skills-last.",
|
|
311
|
+
"error",
|
|
312
|
+
);
|
|
363
313
|
return;
|
|
364
314
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
pi
|
|
315
|
+
const hint = formatLastActivationHint();
|
|
316
|
+
if (hint) ctx.ui.notify(`Repeating: ${hint}`, "info");
|
|
317
|
+
processAndSend(pi, ctx, replay, getSkills);
|
|
368
318
|
},
|
|
369
319
|
});
|
|
370
320
|
|
|
371
|
-
|
|
372
|
-
// Input event — handles legacy formats that use colons/plus signs.
|
|
373
|
-
// These formats bypass the command system, so we intercept via input.
|
|
374
|
-
//
|
|
375
|
-
// Supported:
|
|
376
|
-
// /skills:name1,name2,... [args] (colon + comma)
|
|
377
|
-
// /skill:name1+name2+... [args] (colon + plus)
|
|
378
|
-
// ========================================================================
|
|
379
|
-
pi.on("input", async (event, _ctx) => {
|
|
321
|
+
pi.on("input", async (event, ctx) => {
|
|
380
322
|
const text = event.text.trim();
|
|
381
|
-
|
|
382
|
-
// Only handle /skills:... or /skill:...+... formats
|
|
383
|
-
// /skills ... (space) is handled by the registered command above
|
|
384
|
-
if (!text.startsWith("/skills:") && !text.startsWith("/skill:"))
|
|
323
|
+
if (!text.startsWith("/skills:") && !text.startsWith("/skill:")) {
|
|
385
324
|
return { action: "continue" };
|
|
386
|
-
|
|
387
|
-
// For /skill:... with no plus sign, let pi's built-in handler deal with it
|
|
325
|
+
}
|
|
388
326
|
if (text.startsWith("/skill:") && !text.slice(7).includes("+")) {
|
|
389
327
|
return { action: "continue" };
|
|
390
328
|
}
|
|
391
329
|
|
|
392
|
-
// Parse the skill list
|
|
393
330
|
const colonIndex = text.indexOf(":");
|
|
394
331
|
const spaceIndex = text.indexOf(" ", colonIndex);
|
|
395
332
|
const skillListRaw =
|
|
@@ -397,8 +334,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
397
334
|
? text.slice(colonIndex + 1)
|
|
398
335
|
: text.slice(colonIndex + 1, spaceIndex);
|
|
399
336
|
const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
|
|
400
|
-
|
|
401
|
-
// Determine separator: comma for /skills:, plus for /skill:
|
|
402
337
|
const separator = text.startsWith("/skills:") ? "," : "+";
|
|
403
338
|
const skillNames = skillListRaw
|
|
404
339
|
.split(separator)
|
|
@@ -407,21 +342,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
407
342
|
|
|
408
343
|
if (skillNames.length === 0) return { action: "continue" };
|
|
409
344
|
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
.map((name) => nameToSkill.get(name))
|
|
415
|
-
.filter((s): s is SkillInfo => s !== undefined);
|
|
416
|
-
|
|
417
|
-
const combined = buildCombinedSkills(
|
|
418
|
-
selectedSkills.length > 0 ? selectedSkills : [],
|
|
419
|
-
_ctx.cwd,
|
|
420
|
-
args || undefined,
|
|
345
|
+
const available = getSkills(ctx.cwd);
|
|
346
|
+
const { skills: selected } = resolveSelectedSkillsWithConflicts(
|
|
347
|
+
skillNames,
|
|
348
|
+
available,
|
|
421
349
|
);
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
350
|
+
const blocks = selected
|
|
351
|
+
.map((s) => resolveAndReadLegacySkill(s.name, s.filePath, ctx.cwd))
|
|
352
|
+
.filter((b): b is string => b !== null);
|
|
353
|
+
|
|
354
|
+
if (blocks.length === 0) return { action: "continue" };
|
|
355
|
+
|
|
356
|
+
const wrapped = [
|
|
357
|
+
`<manually_attached_skills count="${blocks.length}">`,
|
|
358
|
+
...blocks,
|
|
359
|
+
args ? `\n<user_query>\n${args}\n</user_query>` : "",
|
|
360
|
+
"</manually_attached_skills>",
|
|
361
|
+
]
|
|
362
|
+
.filter(Boolean)
|
|
363
|
+
.join("\n\n");
|
|
364
|
+
|
|
365
|
+
return { action: "transform", text: wrapped };
|
|
426
366
|
});
|
|
427
367
|
}
|
|
368
|
+
|
|
369
|
+
export { DEFAULT_BUNDLES };
|