@alexgorbatchev/pi-skill-library 0.1.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/LICENSE +21 -0
- package/README.md +88 -0
- package/package.json +49 -0
- package/skills-library/.gitkeep +0 -0
- package/skills-library/opencode-config/SKILL.md +51 -0
- package/skills-library/react-development/SKILL.md +135 -0
- package/skills-library/react-development/references/oxlint.md +144 -0
- package/skills-library/react-development/references/oxlintrc.json +16 -0
- package/skills-library/react-development/references/reactPolicyPlugin.js +549 -0
- package/skills-library/react-development/references/reactPolicyPlugin.test.ts +152 -0
- package/src/createLibraryReport.ts +42 -0
- package/src/discoverLibrarySkills.ts +365 -0
- package/src/expandLibrarySkill.ts +10 -0
- package/src/groupLibrarySummariesByScope.ts +37 -0
- package/src/index.ts +1 -0
- package/src/parseLibraryCommand.ts +22 -0
- package/src/piSkillLibraryExtension.ts +209 -0
- package/src/renderLibraryReport.ts +32 -0
- package/src/replaceHomeDirectoryWithTilde.ts +19 -0
- package/src/types.ts +25 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { groupLibrarySummariesByScope } from './groupLibrarySummariesByScope.js';
|
|
2
|
+
import { replaceHomeDirectoryWithTilde } from './replaceHomeDirectoryWithTilde.js';
|
|
3
|
+
import type { ILibraryReportDetails, ILibrarySkillDiscovery, ILibrarySummary } from './types.js';
|
|
4
|
+
|
|
5
|
+
export function createLibraryReport(librarySkillDiscovery: ILibrarySkillDiscovery): string {
|
|
6
|
+
return createLibraryReportFromDetails({
|
|
7
|
+
diagnostics: librarySkillDiscovery.diagnostics,
|
|
8
|
+
librarySummaries: librarySkillDiscovery.librarySummaries,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createLibraryReportFromDetails(details: ILibraryReportDetails): string {
|
|
13
|
+
const lines = ['[Skills Library]'];
|
|
14
|
+
if (details.librarySummaries.length === 0) {
|
|
15
|
+
lines.push(' No library skills were discovered.');
|
|
16
|
+
return lines.join('\n');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const librarySummariesByScope = groupLibrarySummariesByScope(details.librarySummaries);
|
|
20
|
+
for (const [scope, librarySummaries] of librarySummariesByScope) {
|
|
21
|
+
lines.push(` ${scope}`);
|
|
22
|
+
for (const librarySummary of librarySummaries) {
|
|
23
|
+
appendLibrarySummary(lines, librarySummary);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (details.diagnostics.length > 0) {
|
|
28
|
+
lines.push(' diagnostics');
|
|
29
|
+
for (const diagnostic of details.diagnostics) {
|
|
30
|
+
lines.push(` ${replaceHomeDirectoryWithTilde(diagnostic)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return lines.join('\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function appendLibrarySummary(lines: string[], librarySummary: ILibrarySummary): void {
|
|
38
|
+
lines.push(` ${replaceHomeDirectoryWithTilde(librarySummary.libraryPath)}`);
|
|
39
|
+
for (const skillName of librarySummary.skillNames) {
|
|
40
|
+
lines.push(` /library:${skillName}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DefaultResourceLoader,
|
|
3
|
+
getAgentDir,
|
|
4
|
+
loadSkills,
|
|
5
|
+
type ResourceDiagnostic,
|
|
6
|
+
SettingsManager,
|
|
7
|
+
type Skill,
|
|
8
|
+
} from '@mariozechner/pi-coding-agent';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import type { ILibrarySkillDiscovery, ILibrarySummary } from './types.js';
|
|
13
|
+
|
|
14
|
+
const SETTINGS_KEY = '@alexgorbatchev/pi-skills-library';
|
|
15
|
+
const LIBRARY_DIRECTORY_NAME = 'skills-library';
|
|
16
|
+
|
|
17
|
+
interface IConfiguredLibrarySettings {
|
|
18
|
+
paths: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type LibraryPathScope = 'project' | 'user' | 'temporary';
|
|
22
|
+
|
|
23
|
+
interface ILibraryPathCandidate {
|
|
24
|
+
path: string;
|
|
25
|
+
scope: LibraryPathScope;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function discoverLibrarySkills(
|
|
29
|
+
cwd: string,
|
|
30
|
+
extensionPackageRoot: string,
|
|
31
|
+
): Promise<ILibrarySkillDiscovery> {
|
|
32
|
+
const settingsManager = SettingsManager.create(cwd);
|
|
33
|
+
const projectSettings = settingsManager.getProjectSettings();
|
|
34
|
+
const userSettings = settingsManager.getGlobalSettings();
|
|
35
|
+
const projectSettingsBaseDir = path.join(cwd, '.pi');
|
|
36
|
+
const userSettingsBaseDir = getAgentDir();
|
|
37
|
+
|
|
38
|
+
const projectConfiguredPaths = getConfiguredLibraryPaths(projectSettings, projectSettingsBaseDir, 'project');
|
|
39
|
+
const userConfiguredPaths = getConfiguredLibraryPaths(userSettings, userSettingsBaseDir, 'user');
|
|
40
|
+
const projectConfiguredSkillSiblingPaths = getConfiguredSkillSiblingLibraryPaths(
|
|
41
|
+
projectSettings,
|
|
42
|
+
projectSettingsBaseDir,
|
|
43
|
+
'project',
|
|
44
|
+
);
|
|
45
|
+
const userConfiguredSkillSiblingPaths = getConfiguredSkillSiblingLibraryPaths(
|
|
46
|
+
userSettings,
|
|
47
|
+
userSettingsBaseDir,
|
|
48
|
+
'user',
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const conventionalPaths = getConventionalLibraryPaths(cwd);
|
|
52
|
+
const derivedPaths = await getDerivedLibraryPaths(cwd, extensionPackageRoot);
|
|
53
|
+
|
|
54
|
+
const orderedPaths = dedupeLibraryPaths([
|
|
55
|
+
...filterPathsByScope(projectConfiguredPaths, 'project'),
|
|
56
|
+
...filterPathsByScope(projectConfiguredSkillSiblingPaths, 'project'),
|
|
57
|
+
...filterPathsByScope(conventionalPaths, 'project'),
|
|
58
|
+
...filterPathsByScope(derivedPaths, 'project'),
|
|
59
|
+
...filterPathsByScope(projectConfiguredPaths, 'temporary'),
|
|
60
|
+
...filterPathsByScope(conventionalPaths, 'temporary'),
|
|
61
|
+
...filterPathsByScope(derivedPaths, 'temporary'),
|
|
62
|
+
...filterPathsByScope(userConfiguredPaths, 'user'),
|
|
63
|
+
...filterPathsByScope(userConfiguredSkillSiblingPaths, 'user'),
|
|
64
|
+
...filterPathsByScope(conventionalPaths, 'user'),
|
|
65
|
+
...filterPathsByScope(derivedPaths, 'user'),
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
const existingPaths = orderedPaths.filter((candidate) => existsSync(candidate.path));
|
|
69
|
+
const agentDir = getAgentDir();
|
|
70
|
+
const libraryResult = loadSkills({
|
|
71
|
+
cwd,
|
|
72
|
+
agentDir,
|
|
73
|
+
includeDefaults: false,
|
|
74
|
+
skillPaths: existingPaths.map((candidate) => candidate.path),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const libraryPaths = existingPaths.map((candidate) => candidate.path);
|
|
78
|
+
return {
|
|
79
|
+
skills: libraryResult.skills,
|
|
80
|
+
skillByName: createSkillMap(libraryResult.skills),
|
|
81
|
+
diagnostics: libraryResult.diagnostics.map(formatDiagnostic),
|
|
82
|
+
libraryPaths,
|
|
83
|
+
librarySummaries: createLibrarySummaries(existingPaths, libraryResult.skills),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getConfiguredLibraryPaths(
|
|
88
|
+
settings: object,
|
|
89
|
+
baseDir: string,
|
|
90
|
+
scope: LibraryPathScope,
|
|
91
|
+
): ILibraryPathCandidate[] {
|
|
92
|
+
const configuredSettings = parseConfiguredLibrarySettings(Reflect.get(settings, SETTINGS_KEY));
|
|
93
|
+
return configuredSettings.paths.map((configuredPath) => ({
|
|
94
|
+
path: resolveSettingsPath(configuredPath, baseDir),
|
|
95
|
+
scope,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getConfiguredSkillSiblingLibraryPaths(
|
|
100
|
+
settings: object,
|
|
101
|
+
baseDir: string,
|
|
102
|
+
scope: LibraryPathScope,
|
|
103
|
+
): ILibraryPathCandidate[] {
|
|
104
|
+
return readStringArray(Reflect.get(settings, 'skills'))
|
|
105
|
+
.map((configuredSkillPath) => resolveSettingsPath(configuredSkillPath, baseDir))
|
|
106
|
+
.map((resolvedSkillPath) => toLibraryPathFromSkillPath(resolvedSkillPath))
|
|
107
|
+
.filter((libraryPath): libraryPath is string => libraryPath !== null)
|
|
108
|
+
.map((libraryPath) => ({ path: libraryPath, scope }));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseConfiguredLibrarySettings(value: unknown): IConfiguredLibrarySettings {
|
|
112
|
+
if (!isRecord(value)) {
|
|
113
|
+
return { paths: [] };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
paths: readStringArray(value.paths),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getConventionalLibraryPaths(cwd: string): ILibraryPathCandidate[] {
|
|
122
|
+
const agentDir = getAgentDir();
|
|
123
|
+
const ancestorDirectories = getAncestorDirectories(cwd);
|
|
124
|
+
|
|
125
|
+
const projectPaths = [
|
|
126
|
+
path.join(cwd, '.pi', LIBRARY_DIRECTORY_NAME),
|
|
127
|
+
...ancestorDirectories.map((directoryPath) => path.join(directoryPath, '.agents', LIBRARY_DIRECTORY_NAME)),
|
|
128
|
+
];
|
|
129
|
+
const userPaths = [
|
|
130
|
+
path.join(agentDir, LIBRARY_DIRECTORY_NAME),
|
|
131
|
+
path.join(homedir(), '.agents', LIBRARY_DIRECTORY_NAME),
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
return [
|
|
135
|
+
...projectPaths.map(
|
|
136
|
+
(libraryPath): ILibraryPathCandidate => ({ path: libraryPath, scope: 'project' }),
|
|
137
|
+
),
|
|
138
|
+
...userPaths.map(
|
|
139
|
+
(libraryPath): ILibraryPathCandidate => ({ path: libraryPath, scope: 'user' }),
|
|
140
|
+
),
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function getDerivedLibraryPaths(cwd: string, extensionPackageRoot: string): Promise<ILibraryPathCandidate[]> {
|
|
145
|
+
const agentDir = getAgentDir();
|
|
146
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
147
|
+
cwd,
|
|
148
|
+
agentDir,
|
|
149
|
+
noExtensions: true,
|
|
150
|
+
noPromptTemplates: true,
|
|
151
|
+
noThemes: true,
|
|
152
|
+
});
|
|
153
|
+
await resourceLoader.reload();
|
|
154
|
+
|
|
155
|
+
const skillPaths: ILibraryPathCandidate[] = [];
|
|
156
|
+
const extensionScope = classifyExtensionScope(extensionPackageRoot, cwd);
|
|
157
|
+
skillPaths.push({
|
|
158
|
+
path: path.join(extensionPackageRoot, LIBRARY_DIRECTORY_NAME),
|
|
159
|
+
scope: extensionScope,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
for (const skill of resourceLoader.getSkills().skills) {
|
|
163
|
+
const derivedPath = getSiblingLibraryPath(skill);
|
|
164
|
+
if (derivedPath !== null) {
|
|
165
|
+
skillPaths.push({
|
|
166
|
+
path: derivedPath,
|
|
167
|
+
scope: skill.sourceInfo.scope,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const packageLibraryPath = getPackageLibraryPath(skill);
|
|
172
|
+
if (packageLibraryPath !== null) {
|
|
173
|
+
skillPaths.push({
|
|
174
|
+
path: packageLibraryPath,
|
|
175
|
+
scope: skill.sourceInfo.scope,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return skillPaths;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function classifyExtensionScope(extensionPackageRoot: string, cwd: string): LibraryPathScope {
|
|
184
|
+
const normalizedPackageRoot = path.resolve(extensionPackageRoot);
|
|
185
|
+
const normalizedProjectRoot = path.resolve(cwd);
|
|
186
|
+
const projectPiRoot = path.resolve(cwd, '.pi');
|
|
187
|
+
|
|
188
|
+
if (
|
|
189
|
+
isPathInside(normalizedPackageRoot, normalizedProjectRoot) || isPathInside(normalizedPackageRoot, projectPiRoot)
|
|
190
|
+
) {
|
|
191
|
+
return 'project';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return 'user';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function getSiblingLibraryPath(skill: Skill): string | null {
|
|
198
|
+
return toLibraryPathFromSkillPath(skill.filePath);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function getPackageLibraryPath(skill: Skill): string | null {
|
|
202
|
+
if (skill.sourceInfo.origin !== 'package' || skill.sourceInfo.baseDir === undefined) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return path.join(skill.sourceInfo.baseDir, LIBRARY_DIRECTORY_NAME);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function filterPathsByScope(paths: ILibraryPathCandidate[], scope: LibraryPathScope): ILibraryPathCandidate[] {
|
|
210
|
+
return paths.filter((candidate) => candidate.scope === scope);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function dedupeLibraryPaths(paths: ILibraryPathCandidate[]): ILibraryPathCandidate[] {
|
|
214
|
+
const dedupedPaths: ILibraryPathCandidate[] = [];
|
|
215
|
+
const seenPaths = new Set<string>();
|
|
216
|
+
|
|
217
|
+
for (const candidate of paths) {
|
|
218
|
+
const normalizedPath = path.normalize(candidate.path);
|
|
219
|
+
if (seenPaths.has(normalizedPath)) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
seenPaths.add(normalizedPath);
|
|
224
|
+
dedupedPaths.push({
|
|
225
|
+
path: normalizedPath,
|
|
226
|
+
scope: candidate.scope,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return dedupedPaths;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function getAncestorDirectories(cwd: string): string[] {
|
|
234
|
+
const resolvedCwd = path.resolve(cwd);
|
|
235
|
+
const ancestorDirectories: string[] = [];
|
|
236
|
+
|
|
237
|
+
let currentDirectory = resolvedCwd;
|
|
238
|
+
for (;;) {
|
|
239
|
+
ancestorDirectories.push(currentDirectory);
|
|
240
|
+
|
|
241
|
+
if (existsSync(path.join(currentDirectory, '.git'))) {
|
|
242
|
+
return ancestorDirectories;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
246
|
+
if (parentDirectory === currentDirectory) {
|
|
247
|
+
return ancestorDirectories;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
currentDirectory = parentDirectory;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function createLibrarySummaries(libraryPaths: ILibraryPathCandidate[], skills: Skill[]): ILibrarySummary[] {
|
|
255
|
+
const libraryPathValues = libraryPaths.map((libraryPath) => libraryPath.path);
|
|
256
|
+
return libraryPaths
|
|
257
|
+
.map((libraryPath) => ({
|
|
258
|
+
libraryPath: libraryPath.path,
|
|
259
|
+
scope: libraryPath.scope,
|
|
260
|
+
skillNames: findSkillNamesForLibraryPath(libraryPath.path, libraryPathValues, skills),
|
|
261
|
+
}))
|
|
262
|
+
.filter((librarySummary) => librarySummary.skillNames.length > 0);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function findSkillNamesForLibraryPath(libraryPath: string, libraryPaths: string[], skills: Skill[]): string[] {
|
|
266
|
+
return skills
|
|
267
|
+
.filter((skill) => findOwningLibraryPath(skill, libraryPaths) === libraryPath)
|
|
268
|
+
.map((skill) => skill.name)
|
|
269
|
+
.sort();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function findOwningLibraryPath(skill: Skill, libraryPaths: string[]): string | null {
|
|
273
|
+
const skillDirectoryPath = path.normalize(skill.baseDir);
|
|
274
|
+
const matchingLibraryPaths = libraryPaths
|
|
275
|
+
.filter((libraryPath) => isPathInside(skillDirectoryPath, path.normalize(libraryPath)))
|
|
276
|
+
.sort((leftPath, rightPath) => rightPath.length - leftPath.length);
|
|
277
|
+
|
|
278
|
+
const owningLibraryPath = matchingLibraryPaths[0];
|
|
279
|
+
return owningLibraryPath ?? null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function createSkillMap(skills: Skill[]): Map<string, Skill> {
|
|
283
|
+
const skillByName = new Map<string, Skill>();
|
|
284
|
+
for (const skill of skills) {
|
|
285
|
+
if (!skillByName.has(skill.name)) {
|
|
286
|
+
skillByName.set(skill.name, skill);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return skillByName;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function formatDiagnostic(diagnostic: ResourceDiagnostic): string {
|
|
294
|
+
const location = diagnostic.path ? ` (${diagnostic.path})` : '';
|
|
295
|
+
return `${diagnostic.type}: ${diagnostic.message}${location}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function resolveSettingsPath(configuredPath: string, baseDir: string): string {
|
|
299
|
+
const trimmedPath = configuredPath.trim();
|
|
300
|
+
const expandedPath = expandHomeDirectory(trimmedPath);
|
|
301
|
+
if (path.isAbsolute(expandedPath)) {
|
|
302
|
+
return path.normalize(expandedPath);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return path.resolve(baseDir, expandedPath);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function expandHomeDirectory(configuredPath: string): string {
|
|
309
|
+
if (configuredPath === '~') {
|
|
310
|
+
return homedir();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (configuredPath.startsWith('~/')) {
|
|
314
|
+
return path.join(homedir(), configuredPath.slice(2));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return configuredPath;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function toLibraryPathFromSkillPath(resolvedSkillPath: string): string | null {
|
|
321
|
+
const normalizedSkillPath = path.normalize(resolvedSkillPath);
|
|
322
|
+
const pathSegments = normalizedSkillPath.split(path.sep).filter((segment) => segment.length > 0);
|
|
323
|
+
const skillsSegmentIndex = pathSegments.lastIndexOf('skills');
|
|
324
|
+
if (skillsSegmentIndex === -1) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const leadingSegments = pathSegments.slice(0, skillsSegmentIndex);
|
|
329
|
+
if (normalizedSkillPath.startsWith(path.sep)) {
|
|
330
|
+
return path.join(path.sep, ...leadingSegments, LIBRARY_DIRECTORY_NAME);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return path.join(...leadingSegments, LIBRARY_DIRECTORY_NAME);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function isPathInside(candidatePath: string, containerPath: string): boolean {
|
|
337
|
+
const relativePath = path.relative(containerPath, candidatePath);
|
|
338
|
+
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
342
|
+
return typeof value === 'object' && value !== null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function readStringArray(value: unknown): string[] {
|
|
346
|
+
if (!Array.isArray(value)) {
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const stringValues: string[] = [];
|
|
351
|
+
for (const entry of value) {
|
|
352
|
+
if (typeof entry !== 'string') {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const trimmedEntry = entry.trim();
|
|
357
|
+
if (trimmedEntry.length === 0) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
stringValues.push(trimmedEntry);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return stringValues;
|
|
365
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Skill, stripFrontmatter } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
export async function expandLibrarySkill(skill: Skill, args: string): Promise<string> {
|
|
5
|
+
const content = await readFile(skill.filePath, 'utf8');
|
|
6
|
+
const body = stripFrontmatter(content).trim();
|
|
7
|
+
const skillBlock =
|
|
8
|
+
`<skill name="${skill.name}" location="${skill.filePath}">\nReferences are relative to ${skill.baseDir}.\n\n${body}\n</skill>`;
|
|
9
|
+
return args.length > 0 ? `${skillBlock}\n\n${args}` : skillBlock;
|
|
10
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ILibrarySummary } from './types.js';
|
|
2
|
+
|
|
3
|
+
export function groupLibrarySummariesByScope(librarySummaries: ILibrarySummary[]): Map<string, ILibrarySummary[]> {
|
|
4
|
+
const librarySummariesByScope = new Map<string, ILibrarySummary[]>();
|
|
5
|
+
for (const librarySummary of librarySummaries) {
|
|
6
|
+
const displayScope = toDisplayScope(librarySummary.scope);
|
|
7
|
+
const existingSummaries = librarySummariesByScope.get(displayScope) ?? [];
|
|
8
|
+
existingSummaries.push(librarySummary);
|
|
9
|
+
librarySummariesByScope.set(displayScope, existingSummaries);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const orderedScopes = ['project', 'user', 'path'];
|
|
13
|
+
const orderedLibrarySummariesByScope = new Map<string, ILibrarySummary[]>();
|
|
14
|
+
for (const orderedScope of orderedScopes) {
|
|
15
|
+
const scopedSummaries = librarySummariesByScope.get(orderedScope);
|
|
16
|
+
if (scopedSummaries === undefined) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
orderedLibrarySummariesByScope.set(
|
|
21
|
+
orderedScope,
|
|
22
|
+
[...scopedSummaries].sort((leftSummary, rightSummary) =>
|
|
23
|
+
leftSummary.libraryPath.localeCompare(rightSummary.libraryPath)
|
|
24
|
+
),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return orderedLibrarySummariesByScope;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function toDisplayScope(scope: ILibrarySummary['scope']): string {
|
|
32
|
+
if (scope === 'temporary') {
|
|
33
|
+
return 'path';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return scope;
|
|
37
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './piSkillLibraryExtension.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ILibraryCommand } from './types.js';
|
|
2
|
+
|
|
3
|
+
const LIBRARY_COMMAND_PREFIX = '/library:';
|
|
4
|
+
|
|
5
|
+
export function parseLibraryCommand(text: string): ILibraryCommand | null {
|
|
6
|
+
if (!text.startsWith(LIBRARY_COMMAND_PREFIX)) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const commandBody = text.slice(LIBRARY_COMMAND_PREFIX.length);
|
|
11
|
+
const spaceIndex = commandBody.indexOf(' ');
|
|
12
|
+
const skillName = spaceIndex === -1 ? commandBody.trim() : commandBody.slice(0, spaceIndex).trim();
|
|
13
|
+
if (skillName.length === 0) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const args = spaceIndex === -1 ? '' : commandBody.slice(spaceIndex + 1).trim();
|
|
18
|
+
return {
|
|
19
|
+
skillName,
|
|
20
|
+
args,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, InputEventResult } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { Box, Text } from '@mariozechner/pi-tui';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { createLibraryReport } from './createLibraryReport.js';
|
|
6
|
+
import { discoverLibrarySkills } from './discoverLibrarySkills.js';
|
|
7
|
+
import { expandLibrarySkill } from './expandLibrarySkill.js';
|
|
8
|
+
import { parseLibraryCommand } from './parseLibraryCommand.js';
|
|
9
|
+
import { renderLibraryReport } from './renderLibraryReport.js';
|
|
10
|
+
import type { ILibraryReportDetails, ILibrarySkillDiscovery } from './types.js';
|
|
11
|
+
|
|
12
|
+
const INFO_COMMAND_NAME = 'pi-skill-library';
|
|
13
|
+
const STARTUP_REPORT_MESSAGE_TYPE = 'pi-skill-library.startup-report';
|
|
14
|
+
const extensionPackageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
15
|
+
const handledInputEventResult: InputEventResult = { action: 'handled' };
|
|
16
|
+
|
|
17
|
+
export default function piSkillLibraryExtension(pi: ExtensionAPI): void {
|
|
18
|
+
let cachedLibrarySkillDiscovery: ILibrarySkillDiscovery | null = null;
|
|
19
|
+
let cachedCwd = '';
|
|
20
|
+
|
|
21
|
+
const ensureLibrarySkillCommandsRegistered = (librarySkillDiscovery: ILibrarySkillDiscovery): void => {
|
|
22
|
+
for (const librarySkill of librarySkillDiscovery.skills) {
|
|
23
|
+
const commandName = createLibraryCommandName(librarySkill.name);
|
|
24
|
+
pi.registerCommand(commandName, {
|
|
25
|
+
description: librarySkill.description,
|
|
26
|
+
handler: async (args, ctx) => {
|
|
27
|
+
const refreshedLibrarySkillDiscovery = await refreshLibrarySkillDiscovery(ctx.cwd);
|
|
28
|
+
const requestedSkill = refreshedLibrarySkillDiscovery.skillByName.get(librarySkill.name);
|
|
29
|
+
if (requestedSkill === undefined) {
|
|
30
|
+
if (ctx.hasUI) {
|
|
31
|
+
ctx.ui.notify(
|
|
32
|
+
`Library skill is no longer available: ${librarySkill.name}. Run /reload if discovery changed.`,
|
|
33
|
+
'error',
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const expandedText = await expandLibrarySkill(requestedSkill, args.trim());
|
|
40
|
+
if (ctx.isIdle()) {
|
|
41
|
+
pi.sendUserMessage(expandedText);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pi.sendUserMessage(expandedText, { deliverAs: 'followUp' });
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const refreshLibrarySkillDiscovery = async (cwd: string): Promise<ILibrarySkillDiscovery> => {
|
|
52
|
+
if (cachedLibrarySkillDiscovery !== null && cachedCwd === cwd) {
|
|
53
|
+
return cachedLibrarySkillDiscovery;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
cachedLibrarySkillDiscovery = await discoverLibrarySkills(cwd, extensionPackageRoot);
|
|
57
|
+
cachedCwd = cwd;
|
|
58
|
+
ensureLibrarySkillCommandsRegistered(cachedLibrarySkillDiscovery);
|
|
59
|
+
return cachedLibrarySkillDiscovery;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const invalidateLibrarySkillDiscovery = (): void => {
|
|
63
|
+
cachedLibrarySkillDiscovery = null;
|
|
64
|
+
cachedCwd = '';
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const onSessionChanged = async (_event: unknown, ctx: ExtensionContext): Promise<void> => {
|
|
68
|
+
invalidateLibrarySkillDiscovery();
|
|
69
|
+
await refreshLibrarySkillDiscovery(ctx.cwd);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
pi.registerMessageRenderer(STARTUP_REPORT_MESSAGE_TYPE, (message, _options, theme) => {
|
|
73
|
+
const reportDetails = readLibraryReportDetails(message.details);
|
|
74
|
+
const reportText = reportDetails === null
|
|
75
|
+
? getMessageTextContent(message.content)
|
|
76
|
+
: renderLibraryReport(theme, reportDetails);
|
|
77
|
+
const box = new Box(0, 0);
|
|
78
|
+
box.addChild(new Text(reportText, 0, 0));
|
|
79
|
+
return box;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
83
|
+
invalidateLibrarySkillDiscovery();
|
|
84
|
+
const librarySkillDiscovery = await refreshLibrarySkillDiscovery(ctx.cwd);
|
|
85
|
+
pi.sendMessage({
|
|
86
|
+
customType: STARTUP_REPORT_MESSAGE_TYPE,
|
|
87
|
+
content: createLibraryReport(librarySkillDiscovery),
|
|
88
|
+
display: true,
|
|
89
|
+
details: createLibraryReportDetails(librarySkillDiscovery),
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
pi.on('session_switch', onSessionChanged);
|
|
93
|
+
pi.on('session_fork', onSessionChanged);
|
|
94
|
+
pi.on('session_tree', onSessionChanged);
|
|
95
|
+
|
|
96
|
+
pi.registerCommand(INFO_COMMAND_NAME, {
|
|
97
|
+
description: 'Print the discovered skills-library roots and skills',
|
|
98
|
+
handler: async (_args, ctx) => {
|
|
99
|
+
const librarySkillDiscovery = await refreshLibrarySkillDiscovery(ctx.cwd);
|
|
100
|
+
pi.sendMessage({
|
|
101
|
+
customType: STARTUP_REPORT_MESSAGE_TYPE,
|
|
102
|
+
content: createLibraryReport(librarySkillDiscovery),
|
|
103
|
+
display: true,
|
|
104
|
+
details: createLibraryReportDetails(librarySkillDiscovery),
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
pi.on('context', async (event) => {
|
|
110
|
+
return {
|
|
111
|
+
messages: event.messages.filter((message) => {
|
|
112
|
+
const messageCustomType = 'customType' in message ? message.customType : undefined;
|
|
113
|
+
return messageCustomType !== STARTUP_REPORT_MESSAGE_TYPE;
|
|
114
|
+
}),
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
pi.on('input', async (event, ctx): Promise<InputEventResult | undefined> => {
|
|
119
|
+
const libraryCommand = parseLibraryCommand(event.text);
|
|
120
|
+
if (libraryCommand === null) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const librarySkillDiscovery = await refreshLibrarySkillDiscovery(ctx.cwd);
|
|
125
|
+
const skill = librarySkillDiscovery.skillByName.get(libraryCommand.skillName);
|
|
126
|
+
if (skill === undefined) {
|
|
127
|
+
const availableSkillNames = librarySkillDiscovery.skills.map((librarySkill) => librarySkill.name).sort();
|
|
128
|
+
const availableSkillSummary = availableSkillNames.length === 0
|
|
129
|
+
? 'No library skills are currently available.'
|
|
130
|
+
: `Available library skills: ${availableSkillNames.join(', ')}`;
|
|
131
|
+
|
|
132
|
+
if (ctx.hasUI) {
|
|
133
|
+
ctx.ui.notify(`Unknown library skill: ${libraryCommand.skillName}. ${availableSkillSummary}`, 'error');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return handledInputEventResult;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const expandedText = await expandLibrarySkill(skill, libraryCommand.args);
|
|
140
|
+
return createTransformInputEventResult(expandedText);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function createLibraryReportDetails(librarySkillDiscovery: ILibrarySkillDiscovery): ILibraryReportDetails {
|
|
145
|
+
return {
|
|
146
|
+
diagnostics: librarySkillDiscovery.diagnostics,
|
|
147
|
+
librarySummaries: librarySkillDiscovery.librarySummaries,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function readLibraryReportDetails(details: unknown): ILibraryReportDetails | null {
|
|
152
|
+
if (!isLibraryReportDetails(details)) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return details;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isLibraryReportDetails(value: unknown): value is ILibraryReportDetails {
|
|
160
|
+
if (typeof value !== 'object' || value === null) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const diagnostics = Reflect.get(value, 'diagnostics');
|
|
165
|
+
const librarySummaries = Reflect.get(value, 'librarySummaries');
|
|
166
|
+
return Array.isArray(diagnostics)
|
|
167
|
+
&& diagnostics.every((diagnostic) => typeof diagnostic === 'string')
|
|
168
|
+
&& Array.isArray(librarySummaries)
|
|
169
|
+
&& librarySummaries.every(isLibrarySummary);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isLibrarySummary(value: unknown): value is ILibraryReportDetails['librarySummaries'][number] {
|
|
173
|
+
if (typeof value !== 'object' || value === null) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const libraryPath = Reflect.get(value, 'libraryPath');
|
|
178
|
+
const scope = Reflect.get(value, 'scope');
|
|
179
|
+
const skillNames = Reflect.get(value, 'skillNames');
|
|
180
|
+
return typeof libraryPath === 'string'
|
|
181
|
+
&& isLibrarySummaryScope(scope)
|
|
182
|
+
&& Array.isArray(skillNames)
|
|
183
|
+
&& skillNames.every((skillName) => typeof skillName === 'string');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isLibrarySummaryScope(value: unknown): value is ILibraryReportDetails['librarySummaries'][number]['scope'] {
|
|
187
|
+
return value === 'project' || value === 'user' || value === 'temporary';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function getMessageTextContent(content: string | Array<{ type: string; text?: string; }>): string {
|
|
191
|
+
if (typeof content === 'string') {
|
|
192
|
+
return content;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return content
|
|
196
|
+
.map((part) => ('text' in part && typeof part.text === 'string' ? part.text : '[non-text content omitted]'))
|
|
197
|
+
.join('\n');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function createLibraryCommandName(skillName: string): string {
|
|
201
|
+
return `library:${skillName}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function createTransformInputEventResult(text: string): InputEventResult {
|
|
205
|
+
return {
|
|
206
|
+
action: 'transform',
|
|
207
|
+
text,
|
|
208
|
+
};
|
|
209
|
+
}
|