@alexgorbatchev/pi-skill-library 1.0.0 → 1.0.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 +3 -7
- package/package.json +10 -14
- package/src/createLibraryReport.ts +9 -16
- package/src/discoverLibrarySkills.ts +44 -47
- package/src/expandLibrarySkill.ts +4 -5
- package/src/groupLibrarySummariesByScope.ts +11 -8
- package/src/index.ts +1 -1
- package/src/parseLibraryCommand.ts +4 -4
- package/src/piSkillLibraryExtension.ts +85 -100
- package/src/renderLibraryReport.ts +13 -13
- package/src/replaceHomeDirectoryWithTilde.ts +3 -3
- package/src/types.ts +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @alexgorbatchev/pi-skill-library
|
|
2
2
|
|
|
3
|
-
[pi](https://pi.dev) extension that discovers `skills-library` roots and exposes their skills through `/library:<skill-name>` commands. This extension solves the problem of too many skills. There are skills that you need only occasionally and maybe don't even need them discoverable.
|
|
3
|
+
[pi](https://pi.dev) extension that discovers `skills-library` roots and exposes their skills through `/library:<skill-name>` commands. This extension solves the problem of too many skills. There are skills that you need only occasionally and maybe don't even need them discoverable.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -39,10 +39,7 @@ Use a namespaced block in Pi settings:
|
|
|
39
39
|
```json
|
|
40
40
|
{
|
|
41
41
|
"@alexgorbatchev/pi-skills-library": {
|
|
42
|
-
"paths": [
|
|
43
|
-
"./skills-library",
|
|
44
|
-
"~/shared/pi-skills-library"
|
|
45
|
-
]
|
|
42
|
+
"paths": ["./skills-library", "~/shared/pi-skills-library"]
|
|
46
43
|
}
|
|
47
44
|
}
|
|
48
45
|
```
|
|
@@ -78,11 +75,10 @@ Invoke a library skill directly:
|
|
|
78
75
|
|
|
79
76
|
Discovered library skills are registered as real extension slash commands at startup and reload, so they show up in slash-command autocomplete like other commands.
|
|
80
77
|
|
|
81
|
-
On startup, the extension prints a library-discovery message into the transcript listing each discovered library root and its skills.
|
|
78
|
+
On startup, the extension prints a library-discovery message into the transcript listing each discovered library root and its skills.
|
|
82
79
|
|
|
83
80
|
Use the package info command to print the same report again:
|
|
84
81
|
|
|
85
82
|
```text
|
|
86
83
|
/pi-skill-library
|
|
87
84
|
```
|
|
88
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexgorbatchev/pi-skill-library",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Pi extension that exposes skills-library roots through /library:<skill-name> commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Alex Gorbatchev",
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
"type": "git",
|
|
14
14
|
"url": "git+https://github.com/alexgorbatchev/pi-skill-library.git"
|
|
15
15
|
},
|
|
16
|
-
"packageManager": "bun@1.3.10",
|
|
17
16
|
"keywords": [
|
|
18
17
|
"pi-package",
|
|
19
18
|
"pi-extension",
|
|
@@ -38,20 +37,17 @@
|
|
|
38
37
|
"@mariozechner/pi-coding-agent": "*"
|
|
39
38
|
},
|
|
40
39
|
"devDependencies": {
|
|
41
|
-
"@
|
|
42
|
-
"@
|
|
43
|
-
"@
|
|
44
|
-
"
|
|
45
|
-
"
|
|
40
|
+
"@alexgorbatchev/typescript-ai-policy": "^1.0.5",
|
|
41
|
+
"@mariozechner/pi-coding-agent": "0.65.2",
|
|
42
|
+
"@types/node": "25.5.2",
|
|
43
|
+
"@typescript/native-preview": "7.0.0-dev.20260407.1",
|
|
44
|
+
"oxfmt": "^0.44.0",
|
|
45
|
+
"oxlint": "^1.59.0",
|
|
46
46
|
"typescript": "6.0.2"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"lint": "oxlint --config oxlintrc.json .",
|
|
53
|
-
"format": "dprint fmt",
|
|
54
|
-
"format:check": "dprint check",
|
|
55
|
-
"verify:pi-load": "PI_OFFLINE=1 bun x pi --no-extensions -e ./src/index.ts --list-models > /dev/null 2>&1"
|
|
49
|
+
"check": "bun --bun oxfmt --write . && bun --bun oxlint . && tsgo --noEmit",
|
|
50
|
+
"test": "PI_OFFLINE=1 bun x pi --no-extensions -e ./src/index.ts --list-models > /dev/null 2>&1",
|
|
51
|
+
"test:local": "bun x pi -e ./src/index.ts"
|
|
56
52
|
}
|
|
57
53
|
}
|
|
@@ -1,19 +1,12 @@
|
|
|
1
|
-
import { groupLibrarySummariesByScope } from
|
|
2
|
-
import { replaceHomeDirectoryWithTilde } from
|
|
3
|
-
import type { ILibraryReportDetails,
|
|
1
|
+
import { groupLibrarySummariesByScope } from "./groupLibrarySummariesByScope.js";
|
|
2
|
+
import { replaceHomeDirectoryWithTilde } from "./replaceHomeDirectoryWithTilde.js";
|
|
3
|
+
import type { ILibraryReportDetails, ILibrarySummary } from "./types.js";
|
|
4
4
|
|
|
5
|
-
export function createLibraryReport(
|
|
6
|
-
|
|
7
|
-
diagnostics: librarySkillDiscovery.diagnostics,
|
|
8
|
-
librarySummaries: librarySkillDiscovery.librarySummaries,
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function createLibraryReportFromDetails(details: ILibraryReportDetails): string {
|
|
13
|
-
const lines = ['[Skills Library]'];
|
|
5
|
+
export function createLibraryReport(details: ILibraryReportDetails): string {
|
|
6
|
+
const lines = ["[@alexgorbatchev/pi-skill-library]"];
|
|
14
7
|
if (details.librarySummaries.length === 0) {
|
|
15
|
-
lines.push(
|
|
16
|
-
return lines.join(
|
|
8
|
+
lines.push(" No library skills were discovered.");
|
|
9
|
+
return lines.join("\n");
|
|
17
10
|
}
|
|
18
11
|
|
|
19
12
|
const librarySummariesByScope = groupLibrarySummariesByScope(details.librarySummaries);
|
|
@@ -25,13 +18,13 @@ export function createLibraryReportFromDetails(details: ILibraryReportDetails):
|
|
|
25
18
|
}
|
|
26
19
|
|
|
27
20
|
if (details.diagnostics.length > 0) {
|
|
28
|
-
lines.push(
|
|
21
|
+
lines.push(" diagnostics");
|
|
29
22
|
for (const diagnostic of details.diagnostics) {
|
|
30
23
|
lines.push(` ${replaceHomeDirectoryWithTilde(diagnostic)}`);
|
|
31
24
|
}
|
|
32
25
|
}
|
|
33
26
|
|
|
34
|
-
return lines.join(
|
|
27
|
+
return lines.join("\n");
|
|
35
28
|
}
|
|
36
29
|
|
|
37
30
|
function appendLibrarySummary(lines: string[], librarySummary: ILibrarySummary): void {
|
|
@@ -5,20 +5,20 @@ import {
|
|
|
5
5
|
type ResourceDiagnostic,
|
|
6
6
|
SettingsManager,
|
|
7
7
|
type Skill,
|
|
8
|
-
} from
|
|
9
|
-
import { existsSync } from
|
|
10
|
-
import { homedir } from
|
|
11
|
-
import path from
|
|
12
|
-
import type { ILibrarySkillDiscovery, ILibrarySummary } from
|
|
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
13
|
|
|
14
|
-
const SETTINGS_KEY =
|
|
15
|
-
const LIBRARY_DIRECTORY_NAME =
|
|
14
|
+
const SETTINGS_KEY = "@alexgorbatchev/pi-skills-library";
|
|
15
|
+
const LIBRARY_DIRECTORY_NAME = "skills-library";
|
|
16
16
|
|
|
17
17
|
interface IConfiguredLibrarySettings {
|
|
18
18
|
paths: string[];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
type LibraryPathScope =
|
|
21
|
+
type LibraryPathScope = "project" | "user" | "temporary";
|
|
22
22
|
|
|
23
23
|
interface ILibraryPathCandidate {
|
|
24
24
|
path: string;
|
|
@@ -32,37 +32,37 @@ export async function discoverLibrarySkills(
|
|
|
32
32
|
const settingsManager = SettingsManager.create(cwd);
|
|
33
33
|
const projectSettings = settingsManager.getProjectSettings();
|
|
34
34
|
const userSettings = settingsManager.getGlobalSettings();
|
|
35
|
-
const projectSettingsBaseDir = path.join(cwd,
|
|
35
|
+
const projectSettingsBaseDir = path.join(cwd, ".pi");
|
|
36
36
|
const userSettingsBaseDir = getAgentDir();
|
|
37
37
|
|
|
38
|
-
const projectConfiguredPaths = getConfiguredLibraryPaths(projectSettings, projectSettingsBaseDir,
|
|
39
|
-
const userConfiguredPaths = getConfiguredLibraryPaths(userSettings, userSettingsBaseDir,
|
|
38
|
+
const projectConfiguredPaths = getConfiguredLibraryPaths(projectSettings, projectSettingsBaseDir, "project");
|
|
39
|
+
const userConfiguredPaths = getConfiguredLibraryPaths(userSettings, userSettingsBaseDir, "user");
|
|
40
40
|
const projectConfiguredSkillSiblingPaths = getConfiguredSkillSiblingLibraryPaths(
|
|
41
41
|
projectSettings,
|
|
42
42
|
projectSettingsBaseDir,
|
|
43
|
-
|
|
43
|
+
"project",
|
|
44
44
|
);
|
|
45
45
|
const userConfiguredSkillSiblingPaths = getConfiguredSkillSiblingLibraryPaths(
|
|
46
46
|
userSettings,
|
|
47
47
|
userSettingsBaseDir,
|
|
48
|
-
|
|
48
|
+
"user",
|
|
49
49
|
);
|
|
50
50
|
|
|
51
51
|
const conventionalPaths = getConventionalLibraryPaths(cwd);
|
|
52
52
|
const derivedPaths = await getDerivedLibraryPaths(cwd, extensionPackageRoot);
|
|
53
53
|
|
|
54
54
|
const orderedPaths = dedupeLibraryPaths([
|
|
55
|
-
...filterPathsByScope(projectConfiguredPaths,
|
|
56
|
-
...filterPathsByScope(projectConfiguredSkillSiblingPaths,
|
|
57
|
-
...filterPathsByScope(conventionalPaths,
|
|
58
|
-
...filterPathsByScope(derivedPaths,
|
|
59
|
-
...filterPathsByScope(projectConfiguredPaths,
|
|
60
|
-
...filterPathsByScope(conventionalPaths,
|
|
61
|
-
...filterPathsByScope(derivedPaths,
|
|
62
|
-
...filterPathsByScope(userConfiguredPaths,
|
|
63
|
-
...filterPathsByScope(userConfiguredSkillSiblingPaths,
|
|
64
|
-
...filterPathsByScope(conventionalPaths,
|
|
65
|
-
...filterPathsByScope(derivedPaths,
|
|
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
66
|
]);
|
|
67
67
|
|
|
68
68
|
const existingPaths = orderedPaths.filter((candidate) => existsSync(candidate.path));
|
|
@@ -101,7 +101,7 @@ function getConfiguredSkillSiblingLibraryPaths(
|
|
|
101
101
|
baseDir: string,
|
|
102
102
|
scope: LibraryPathScope,
|
|
103
103
|
): ILibraryPathCandidate[] {
|
|
104
|
-
return readStringArray(Reflect.get(settings,
|
|
104
|
+
return readStringArray(Reflect.get(settings, "skills"))
|
|
105
105
|
.map((configuredSkillPath) => resolveSettingsPath(configuredSkillPath, baseDir))
|
|
106
106
|
.map((resolvedSkillPath) => toLibraryPathFromSkillPath(resolvedSkillPath))
|
|
107
107
|
.filter((libraryPath): libraryPath is string => libraryPath !== null)
|
|
@@ -123,21 +123,17 @@ function getConventionalLibraryPaths(cwd: string): ILibraryPathCandidate[] {
|
|
|
123
123
|
const ancestorDirectories = getAncestorDirectories(cwd);
|
|
124
124
|
|
|
125
125
|
const projectPaths = [
|
|
126
|
-
path.join(cwd,
|
|
127
|
-
...ancestorDirectories.map((directoryPath) => path.join(directoryPath,
|
|
126
|
+
path.join(cwd, ".pi", LIBRARY_DIRECTORY_NAME),
|
|
127
|
+
...ancestorDirectories.map((directoryPath) => path.join(directoryPath, ".agents", LIBRARY_DIRECTORY_NAME)),
|
|
128
128
|
];
|
|
129
129
|
const userPaths = [
|
|
130
130
|
path.join(agentDir, LIBRARY_DIRECTORY_NAME),
|
|
131
|
-
path.join(homedir(),
|
|
131
|
+
path.join(homedir(), ".agents", LIBRARY_DIRECTORY_NAME),
|
|
132
132
|
];
|
|
133
133
|
|
|
134
134
|
return [
|
|
135
|
-
...projectPaths.map(
|
|
136
|
-
|
|
137
|
-
),
|
|
138
|
-
...userPaths.map(
|
|
139
|
-
(libraryPath): ILibraryPathCandidate => ({ path: libraryPath, scope: 'user' }),
|
|
140
|
-
),
|
|
135
|
+
...projectPaths.map((libraryPath): ILibraryPathCandidate => ({ path: libraryPath, scope: "project" })),
|
|
136
|
+
...userPaths.map((libraryPath): ILibraryPathCandidate => ({ path: libraryPath, scope: "user" })),
|
|
141
137
|
];
|
|
142
138
|
}
|
|
143
139
|
|
|
@@ -183,15 +179,16 @@ async function getDerivedLibraryPaths(cwd: string, extensionPackageRoot: string)
|
|
|
183
179
|
function classifyExtensionScope(extensionPackageRoot: string, cwd: string): LibraryPathScope {
|
|
184
180
|
const normalizedPackageRoot = path.resolve(extensionPackageRoot);
|
|
185
181
|
const normalizedProjectRoot = path.resolve(cwd);
|
|
186
|
-
const projectPiRoot = path.resolve(cwd,
|
|
182
|
+
const projectPiRoot = path.resolve(cwd, ".pi");
|
|
187
183
|
|
|
188
184
|
if (
|
|
189
|
-
isPathInside(normalizedPackageRoot, normalizedProjectRoot) ||
|
|
185
|
+
isPathInside(normalizedPackageRoot, normalizedProjectRoot) ||
|
|
186
|
+
isPathInside(normalizedPackageRoot, projectPiRoot)
|
|
190
187
|
) {
|
|
191
|
-
return
|
|
188
|
+
return "project";
|
|
192
189
|
}
|
|
193
190
|
|
|
194
|
-
return
|
|
191
|
+
return "user";
|
|
195
192
|
}
|
|
196
193
|
|
|
197
194
|
function getSiblingLibraryPath(skill: Skill): string | null {
|
|
@@ -199,7 +196,7 @@ function getSiblingLibraryPath(skill: Skill): string | null {
|
|
|
199
196
|
}
|
|
200
197
|
|
|
201
198
|
function getPackageLibraryPath(skill: Skill): string | null {
|
|
202
|
-
if (skill.sourceInfo.origin !==
|
|
199
|
+
if (skill.sourceInfo.origin !== "package" || skill.sourceInfo.baseDir === undefined) {
|
|
203
200
|
return null;
|
|
204
201
|
}
|
|
205
202
|
|
|
@@ -238,7 +235,7 @@ function getAncestorDirectories(cwd: string): string[] {
|
|
|
238
235
|
for (;;) {
|
|
239
236
|
ancestorDirectories.push(currentDirectory);
|
|
240
237
|
|
|
241
|
-
if (existsSync(path.join(currentDirectory,
|
|
238
|
+
if (existsSync(path.join(currentDirectory, ".git"))) {
|
|
242
239
|
return ancestorDirectories;
|
|
243
240
|
}
|
|
244
241
|
|
|
@@ -291,7 +288,7 @@ function createSkillMap(skills: Skill[]): Map<string, Skill> {
|
|
|
291
288
|
}
|
|
292
289
|
|
|
293
290
|
function formatDiagnostic(diagnostic: ResourceDiagnostic): string {
|
|
294
|
-
const location = diagnostic.path ? ` (${diagnostic.path})` :
|
|
291
|
+
const location = diagnostic.path ? ` (${diagnostic.path})` : "";
|
|
295
292
|
return `${diagnostic.type}: ${diagnostic.message}${location}`;
|
|
296
293
|
}
|
|
297
294
|
|
|
@@ -306,11 +303,11 @@ function resolveSettingsPath(configuredPath: string, baseDir: string): string {
|
|
|
306
303
|
}
|
|
307
304
|
|
|
308
305
|
function expandHomeDirectory(configuredPath: string): string {
|
|
309
|
-
if (configuredPath ===
|
|
306
|
+
if (configuredPath === "~") {
|
|
310
307
|
return homedir();
|
|
311
308
|
}
|
|
312
309
|
|
|
313
|
-
if (configuredPath.startsWith(
|
|
310
|
+
if (configuredPath.startsWith("~/")) {
|
|
314
311
|
return path.join(homedir(), configuredPath.slice(2));
|
|
315
312
|
}
|
|
316
313
|
|
|
@@ -320,7 +317,7 @@ function expandHomeDirectory(configuredPath: string): string {
|
|
|
320
317
|
function toLibraryPathFromSkillPath(resolvedSkillPath: string): string | null {
|
|
321
318
|
const normalizedSkillPath = path.normalize(resolvedSkillPath);
|
|
322
319
|
const pathSegments = normalizedSkillPath.split(path.sep).filter((segment) => segment.length > 0);
|
|
323
|
-
const skillsSegmentIndex = pathSegments.lastIndexOf(
|
|
320
|
+
const skillsSegmentIndex = pathSegments.lastIndexOf("skills");
|
|
324
321
|
if (skillsSegmentIndex === -1) {
|
|
325
322
|
return null;
|
|
326
323
|
}
|
|
@@ -335,11 +332,11 @@ function toLibraryPathFromSkillPath(resolvedSkillPath: string): string | null {
|
|
|
335
332
|
|
|
336
333
|
function isPathInside(candidatePath: string, containerPath: string): boolean {
|
|
337
334
|
const relativePath = path.relative(containerPath, candidatePath);
|
|
338
|
-
return relativePath ===
|
|
335
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
339
336
|
}
|
|
340
337
|
|
|
341
338
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
342
|
-
return typeof value ===
|
|
339
|
+
return typeof value === "object" && value !== null;
|
|
343
340
|
}
|
|
344
341
|
|
|
345
342
|
function readStringArray(value: unknown): string[] {
|
|
@@ -349,7 +346,7 @@ function readStringArray(value: unknown): string[] {
|
|
|
349
346
|
|
|
350
347
|
const stringValues: string[] = [];
|
|
351
348
|
for (const entry of value) {
|
|
352
|
-
if (typeof entry !==
|
|
349
|
+
if (typeof entry !== "string") {
|
|
353
350
|
continue;
|
|
354
351
|
}
|
|
355
352
|
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { type Skill, stripFrontmatter } from
|
|
2
|
-
import { readFile } from
|
|
1
|
+
import { type Skill, stripFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
3
|
|
|
4
4
|
export async function expandLibrarySkill(skill: Skill, args: string): Promise<string> {
|
|
5
|
-
const content = await readFile(skill.filePath,
|
|
5
|
+
const content = await readFile(skill.filePath, "utf8");
|
|
6
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>`;
|
|
7
|
+
const skillBlock = `<skill name="${skill.name}" location="${skill.filePath}">\nReferences are relative to ${skill.baseDir}.\n\n${body}\n</skill>`;
|
|
9
8
|
return args.length > 0 ? `${skillBlock}\n\n${args}` : skillBlock;
|
|
10
9
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ILibrarySummary } from
|
|
1
|
+
import type { ILibrarySummary } from "./types.js";
|
|
2
2
|
|
|
3
3
|
export function groupLibrarySummariesByScope(librarySummaries: ILibrarySummary[]): Map<string, ILibrarySummary[]> {
|
|
4
4
|
const librarySummariesByScope = new Map<string, ILibrarySummary[]>();
|
|
@@ -9,7 +9,7 @@ export function groupLibrarySummariesByScope(librarySummaries: ILibrarySummary[]
|
|
|
9
9
|
librarySummariesByScope.set(displayScope, existingSummaries);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const orderedScopes = [
|
|
12
|
+
const orderedScopes = ["project settings", "global settings", "path"];
|
|
13
13
|
const orderedLibrarySummariesByScope = new Map<string, ILibrarySummary[]>();
|
|
14
14
|
for (const orderedScope of orderedScopes) {
|
|
15
15
|
const scopedSummaries = librarySummariesByScope.get(orderedScope);
|
|
@@ -20,7 +20,7 @@ export function groupLibrarySummariesByScope(librarySummaries: ILibrarySummary[]
|
|
|
20
20
|
orderedLibrarySummariesByScope.set(
|
|
21
21
|
orderedScope,
|
|
22
22
|
[...scopedSummaries].sort((leftSummary, rightSummary) =>
|
|
23
|
-
leftSummary.libraryPath.localeCompare(rightSummary.libraryPath)
|
|
23
|
+
leftSummary.libraryPath.localeCompare(rightSummary.libraryPath),
|
|
24
24
|
),
|
|
25
25
|
);
|
|
26
26
|
}
|
|
@@ -28,10 +28,13 @@ export function groupLibrarySummariesByScope(librarySummaries: ILibrarySummary[]
|
|
|
28
28
|
return orderedLibrarySummariesByScope;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function toDisplayScope(scope: ILibrarySummary[
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
function toDisplayScope(scope: ILibrarySummary["scope"]): string {
|
|
32
|
+
switch (scope) {
|
|
33
|
+
case "project":
|
|
34
|
+
return "project settings";
|
|
35
|
+
case "user":
|
|
36
|
+
return "global settings";
|
|
37
|
+
case "temporary":
|
|
38
|
+
return "path";
|
|
34
39
|
}
|
|
35
|
-
|
|
36
|
-
return scope;
|
|
37
40
|
}
|
package/src/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default } from
|
|
1
|
+
export { default } from "./piSkillLibraryExtension.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ILibraryCommand } from
|
|
1
|
+
import type { ILibraryCommand } from "./types.js";
|
|
2
2
|
|
|
3
|
-
const LIBRARY_COMMAND_PREFIX =
|
|
3
|
+
const LIBRARY_COMMAND_PREFIX = "/library:";
|
|
4
4
|
|
|
5
5
|
export function parseLibraryCommand(text: string): ILibraryCommand | null {
|
|
6
6
|
if (!text.startsWith(LIBRARY_COMMAND_PREFIX)) {
|
|
@@ -8,13 +8,13 @@ export function parseLibraryCommand(text: string): ILibraryCommand | null {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const commandBody = text.slice(LIBRARY_COMMAND_PREFIX.length);
|
|
11
|
-
const spaceIndex = commandBody.indexOf(
|
|
11
|
+
const spaceIndex = commandBody.indexOf(" ");
|
|
12
12
|
const skillName = spaceIndex === -1 ? commandBody.trim() : commandBody.slice(0, spaceIndex).trim();
|
|
13
13
|
if (skillName.length === 0) {
|
|
14
14
|
return null;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const args = spaceIndex === -1 ?
|
|
17
|
+
const args = spaceIndex === -1 ? "" : commandBody.slice(spaceIndex + 1).trim();
|
|
18
18
|
return {
|
|
19
19
|
skillName,
|
|
20
20
|
args,
|
|
@@ -1,22 +1,59 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
1
|
+
import type { ImageContent, TextContent } from "@mariozechner/pi-ai";
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext, InputEventResult, Theme } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { Box, Text } from "@mariozechner/pi-tui";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { createLibraryReport } from "./createLibraryReport.js";
|
|
7
|
+
import { discoverLibrarySkills } from "./discoverLibrarySkills.js";
|
|
8
|
+
import { expandLibrarySkill } from "./expandLibrarySkill.js";
|
|
9
|
+
import { parseLibraryCommand } from "./parseLibraryCommand.js";
|
|
10
|
+
import { renderLibraryReport } from "./renderLibraryReport.js";
|
|
11
|
+
import type { ILibraryReportDetails, ILibrarySkillDiscovery } from "./types.js";
|
|
12
|
+
|
|
13
|
+
const INFO_COMMAND_NAME = "pi-skill-library";
|
|
14
|
+
const LIBRARY_MESSAGE_TYPE = "pi-skill-library.message";
|
|
14
15
|
const extensionPackageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
15
|
-
const handledInputEventResult: InputEventResult = { action:
|
|
16
|
+
const handledInputEventResult: InputEventResult = { action: "handled" };
|
|
17
|
+
|
|
18
|
+
type LibraryMessageContent = string | (TextContent | ImageContent)[];
|
|
19
|
+
type LibraryMessageLevel = "info" | "error";
|
|
20
|
+
|
|
21
|
+
interface ILibraryMessageDetails {
|
|
22
|
+
level: LibraryMessageLevel;
|
|
23
|
+
reportDetails?: ILibraryReportDetails;
|
|
24
|
+
}
|
|
16
25
|
|
|
17
26
|
export default function piSkillLibraryExtension(pi: ExtensionAPI): void {
|
|
18
27
|
let cachedLibrarySkillDiscovery: ILibrarySkillDiscovery | null = null;
|
|
19
|
-
let cachedCwd =
|
|
28
|
+
let cachedCwd = "";
|
|
29
|
+
|
|
30
|
+
pi.registerMessageRenderer<ILibraryMessageDetails>(LIBRARY_MESSAGE_TYPE, (message, _options, theme) => {
|
|
31
|
+
const details = message.details;
|
|
32
|
+
const content = getMessageTextContent(message.content);
|
|
33
|
+
const text =
|
|
34
|
+
details?.reportDetails === undefined
|
|
35
|
+
? renderLibraryMessage(theme, content, details?.level ?? "info")
|
|
36
|
+
: renderLibraryReport(theme, details.reportDetails);
|
|
37
|
+
const box = new Box(0, 0);
|
|
38
|
+
box.addChild(new Text(text, 0, 0));
|
|
39
|
+
return box;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const sendLibraryMessage = (content: string, details: ILibraryMessageDetails): void => {
|
|
43
|
+
pi.sendMessage<ILibraryMessageDetails>({
|
|
44
|
+
customType: LIBRARY_MESSAGE_TYPE,
|
|
45
|
+
content,
|
|
46
|
+
display: true,
|
|
47
|
+
details,
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const sendLibraryReport = (reportDetails: ILibraryReportDetails): void => {
|
|
52
|
+
sendLibraryMessage(createLibraryReport(reportDetails), {
|
|
53
|
+
level: "info",
|
|
54
|
+
reportDetails,
|
|
55
|
+
});
|
|
56
|
+
};
|
|
20
57
|
|
|
21
58
|
const ensureLibrarySkillCommandsRegistered = (librarySkillDiscovery: ILibrarySkillDiscovery): void => {
|
|
22
59
|
for (const librarySkill of librarySkillDiscovery.skills) {
|
|
@@ -28,9 +65,9 @@ export default function piSkillLibraryExtension(pi: ExtensionAPI): void {
|
|
|
28
65
|
const requestedSkill = refreshedLibrarySkillDiscovery.skillByName.get(librarySkill.name);
|
|
29
66
|
if (requestedSkill === undefined) {
|
|
30
67
|
if (ctx.hasUI) {
|
|
31
|
-
|
|
68
|
+
sendLibraryMessage(
|
|
32
69
|
`Library skill is no longer available: ${librarySkill.name}. Run /reload if discovery changed.`,
|
|
33
|
-
|
|
70
|
+
{ level: "error" },
|
|
34
71
|
);
|
|
35
72
|
}
|
|
36
73
|
return;
|
|
@@ -42,7 +79,7 @@ export default function piSkillLibraryExtension(pi: ExtensionAPI): void {
|
|
|
42
79
|
return;
|
|
43
80
|
}
|
|
44
81
|
|
|
45
|
-
pi.sendUserMessage(expandedText, { deliverAs:
|
|
82
|
+
pi.sendUserMessage(expandedText, { deliverAs: "followUp" });
|
|
46
83
|
},
|
|
47
84
|
});
|
|
48
85
|
}
|
|
@@ -61,7 +98,7 @@ export default function piSkillLibraryExtension(pi: ExtensionAPI): void {
|
|
|
61
98
|
|
|
62
99
|
const invalidateLibrarySkillDiscovery = (): void => {
|
|
63
100
|
cachedLibrarySkillDiscovery = null;
|
|
64
|
-
cachedCwd =
|
|
101
|
+
cachedCwd = "";
|
|
65
102
|
};
|
|
66
103
|
|
|
67
104
|
const onSessionChanged = async (_event: unknown, ctx: ExtensionContext): Promise<void> => {
|
|
@@ -69,53 +106,29 @@ export default function piSkillLibraryExtension(pi: ExtensionAPI): void {
|
|
|
69
106
|
await refreshLibrarySkillDiscovery(ctx.cwd);
|
|
70
107
|
};
|
|
71
108
|
|
|
72
|
-
pi.
|
|
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) => {
|
|
109
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
83
110
|
invalidateLibrarySkillDiscovery();
|
|
84
111
|
const librarySkillDiscovery = await refreshLibrarySkillDiscovery(ctx.cwd);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
details: createLibraryReportDetails(librarySkillDiscovery),
|
|
90
|
-
});
|
|
112
|
+
|
|
113
|
+
if (ctx.hasUI) {
|
|
114
|
+
sendLibraryReport(createLibraryReportDetails(librarySkillDiscovery));
|
|
115
|
+
}
|
|
91
116
|
});
|
|
92
|
-
pi.on(
|
|
93
|
-
pi.on(
|
|
94
|
-
pi.on(
|
|
117
|
+
pi.on("session_before_switch", onSessionChanged);
|
|
118
|
+
pi.on("session_before_fork", onSessionChanged);
|
|
119
|
+
pi.on("session_before_tree", onSessionChanged);
|
|
95
120
|
|
|
96
121
|
pi.registerCommand(INFO_COMMAND_NAME, {
|
|
97
|
-
description:
|
|
122
|
+
description: "Print the discovered skills-library roots and skills",
|
|
98
123
|
handler: async (_args, ctx) => {
|
|
99
124
|
const librarySkillDiscovery = await refreshLibrarySkillDiscovery(ctx.cwd);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
display: true,
|
|
104
|
-
details: createLibraryReportDetails(librarySkillDiscovery),
|
|
105
|
-
});
|
|
125
|
+
if (ctx.hasUI) {
|
|
126
|
+
sendLibraryReport(createLibraryReportDetails(librarySkillDiscovery));
|
|
127
|
+
}
|
|
106
128
|
},
|
|
107
129
|
});
|
|
108
130
|
|
|
109
|
-
pi.on(
|
|
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> => {
|
|
131
|
+
pi.on("input", async (event, ctx): Promise<InputEventResult | undefined> => {
|
|
119
132
|
const libraryCommand = parseLibraryCommand(event.text);
|
|
120
133
|
if (libraryCommand === null) {
|
|
121
134
|
return undefined;
|
|
@@ -125,12 +138,15 @@ export default function piSkillLibraryExtension(pi: ExtensionAPI): void {
|
|
|
125
138
|
const skill = librarySkillDiscovery.skillByName.get(libraryCommand.skillName);
|
|
126
139
|
if (skill === undefined) {
|
|
127
140
|
const availableSkillNames = librarySkillDiscovery.skills.map((librarySkill) => librarySkill.name).sort();
|
|
128
|
-
const availableSkillSummary =
|
|
129
|
-
|
|
130
|
-
|
|
141
|
+
const availableSkillSummary =
|
|
142
|
+
availableSkillNames.length === 0
|
|
143
|
+
? "No library skills are currently available."
|
|
144
|
+
: `Available library skills: ${availableSkillNames.join(", ")}`;
|
|
131
145
|
|
|
132
146
|
if (ctx.hasUI) {
|
|
133
|
-
|
|
147
|
+
sendLibraryMessage(`Unknown library skill: ${libraryCommand.skillName}. ${availableSkillSummary}`, {
|
|
148
|
+
level: "error",
|
|
149
|
+
});
|
|
134
150
|
}
|
|
135
151
|
|
|
136
152
|
return handledInputEventResult;
|
|
@@ -148,53 +164,22 @@ function createLibraryReportDetails(librarySkillDiscovery: ILibrarySkillDiscover
|
|
|
148
164
|
};
|
|
149
165
|
}
|
|
150
166
|
|
|
151
|
-
function
|
|
152
|
-
if (
|
|
153
|
-
return
|
|
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;
|
|
167
|
+
function renderLibraryMessage(theme: Theme, content: string, level: LibraryMessageLevel): string {
|
|
168
|
+
if (level === "error") {
|
|
169
|
+
return theme.fg("error", content);
|
|
162
170
|
}
|
|
163
171
|
|
|
164
|
-
|
|
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';
|
|
172
|
+
return content;
|
|
188
173
|
}
|
|
189
174
|
|
|
190
|
-
function getMessageTextContent(content:
|
|
191
|
-
if (typeof content ===
|
|
175
|
+
function getMessageTextContent(content: LibraryMessageContent): string {
|
|
176
|
+
if (typeof content === "string") {
|
|
192
177
|
return content;
|
|
193
178
|
}
|
|
194
179
|
|
|
195
180
|
return content
|
|
196
|
-
.map((part) => (
|
|
197
|
-
.join(
|
|
181
|
+
.map((part) => ("text" in part && typeof part.text === "string" ? part.text : "[non-text content omitted]"))
|
|
182
|
+
.join("\n");
|
|
198
183
|
}
|
|
199
184
|
|
|
200
185
|
function createLibraryCommandName(skillName: string): string {
|
|
@@ -203,7 +188,7 @@ function createLibraryCommandName(skillName: string): string {
|
|
|
203
188
|
|
|
204
189
|
function createTransformInputEventResult(text: string): InputEventResult {
|
|
205
190
|
return {
|
|
206
|
-
action:
|
|
191
|
+
action: "transform",
|
|
207
192
|
text,
|
|
208
193
|
};
|
|
209
194
|
}
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import type { Theme } from
|
|
2
|
-
import { groupLibrarySummariesByScope } from
|
|
3
|
-
import { replaceHomeDirectoryWithTilde } from
|
|
4
|
-
import type { ILibraryReportDetails } from
|
|
1
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { groupLibrarySummariesByScope } from "./groupLibrarySummariesByScope.js";
|
|
3
|
+
import { replaceHomeDirectoryWithTilde } from "./replaceHomeDirectoryWithTilde.js";
|
|
4
|
+
import type { ILibraryReportDetails } from "./types.js";
|
|
5
5
|
|
|
6
6
|
export function renderLibraryReport(theme: Theme, details: ILibraryReportDetails): string {
|
|
7
|
-
const lines: string[] = [theme.fg(
|
|
7
|
+
const lines: string[] = [theme.fg("mdHeading", "[@alexgorbatchev/pi-skill-library]")];
|
|
8
8
|
if (details.librarySummaries.length === 0) {
|
|
9
|
-
lines.push(theme.fg(
|
|
10
|
-
return lines.join(
|
|
9
|
+
lines.push(theme.fg("dim", " No library skills were discovered."));
|
|
10
|
+
return lines.join("\n");
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const groupedSummaries = groupLibrarySummariesByScope(details.librarySummaries);
|
|
14
14
|
for (const [scope, librarySummaries] of groupedSummaries) {
|
|
15
|
-
lines.push(` ${theme.fg(
|
|
15
|
+
lines.push(` ${theme.fg("accent", scope)}`);
|
|
16
16
|
for (const librarySummary of librarySummaries) {
|
|
17
|
-
lines.push(theme.fg(
|
|
17
|
+
lines.push(theme.fg("dim", ` ${replaceHomeDirectoryWithTilde(librarySummary.libraryPath)}`));
|
|
18
18
|
for (const skillName of librarySummary.skillNames) {
|
|
19
|
-
lines.push(theme.fg(
|
|
19
|
+
lines.push(theme.fg("dim", ` /library:${skillName}`));
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
if (details.diagnostics.length > 0) {
|
|
25
|
-
lines.push(` ${theme.fg(
|
|
25
|
+
lines.push(` ${theme.fg("warning", "diagnostics")}`);
|
|
26
26
|
for (const diagnostic of details.diagnostics) {
|
|
27
|
-
lines.push(theme.fg(
|
|
27
|
+
lines.push(theme.fg("warning", ` ${replaceHomeDirectoryWithTilde(diagnostic)}`));
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
return lines.join(
|
|
31
|
+
return lines.join("\n");
|
|
32
32
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { homedir } from
|
|
2
|
-
import path from
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
export function replaceHomeDirectoryWithTilde(value: string): string {
|
|
5
5
|
const homeDirectoryPath = homedir();
|
|
@@ -7,7 +7,7 @@ export function replaceHomeDirectoryWithTilde(value: string): string {
|
|
|
7
7
|
const normalizedValue = path.normalize(value);
|
|
8
8
|
|
|
9
9
|
if (normalizedValue === normalizedHomeDirectoryPath) {
|
|
10
|
-
return
|
|
10
|
+
return "~";
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const homeDirectoryPrefix = `${normalizedHomeDirectoryPath}${path.sep}`;
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Skill } from
|
|
1
|
+
import type { Skill } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
export interface ILibraryCommand {
|
|
4
4
|
skillName: string;
|
|
@@ -7,7 +7,7 @@ export interface ILibraryCommand {
|
|
|
7
7
|
|
|
8
8
|
export interface ILibrarySummary {
|
|
9
9
|
libraryPath: string;
|
|
10
|
-
scope:
|
|
10
|
+
scope: "project" | "user" | "temporary";
|
|
11
11
|
skillNames: string[];
|
|
12
12
|
}
|
|
13
13
|
|