@alexgorbatchev/pi-skill-library 1.0.0 → 1.0.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 +2 -6
- package/package.json +10 -14
- package/src/createLibraryReport.ts +8 -8
- package/src/discoverLibrarySkills.ts +44 -47
- package/src/expandLibrarySkill.ts +4 -5
- package/src/groupLibrarySummariesByScope.ts +6 -6
- package/src/index.ts +1 -1
- package/src/parseLibraryCommand.ts +4 -4
- package/src/piSkillLibraryExtension.ts +60 -56
- package/src/renderLibraryReport.ts +13 -13
- package/src/replaceHomeDirectoryWithTilde.ts +3 -3
- package/src/types.ts +9 -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
|
```
|
|
@@ -85,4 +82,3 @@ Use the package info command to print the same report again:
|
|
|
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.1",
|
|
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,6 +1,6 @@
|
|
|
1
|
-
import { groupLibrarySummariesByScope } from
|
|
2
|
-
import { replaceHomeDirectoryWithTilde } from
|
|
3
|
-
import type { ILibraryReportDetails, ILibrarySkillDiscovery, ILibrarySummary } from
|
|
1
|
+
import { groupLibrarySummariesByScope } from "./groupLibrarySummariesByScope.js";
|
|
2
|
+
import { replaceHomeDirectoryWithTilde } from "./replaceHomeDirectoryWithTilde.js";
|
|
3
|
+
import type { ILibraryReportDetails, ILibrarySkillDiscovery, ILibrarySummary } from "./types.js";
|
|
4
4
|
|
|
5
5
|
export function createLibraryReport(librarySkillDiscovery: ILibrarySkillDiscovery): string {
|
|
6
6
|
return createLibraryReportFromDetails({
|
|
@@ -10,10 +10,10 @@ export function createLibraryReport(librarySkillDiscovery: ILibrarySkillDiscover
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function createLibraryReportFromDetails(details: ILibraryReportDetails): string {
|
|
13
|
-
const lines = [
|
|
13
|
+
const lines = ["[Skills Library]"];
|
|
14
14
|
if (details.librarySummaries.length === 0) {
|
|
15
|
-
lines.push(
|
|
16
|
-
return lines.join(
|
|
15
|
+
lines.push(" No library skills were discovered.");
|
|
16
|
+
return lines.join("\n");
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const librarySummariesByScope = groupLibrarySummariesByScope(details.librarySummaries);
|
|
@@ -25,13 +25,13 @@ export function createLibraryReportFromDetails(details: ILibraryReportDetails):
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
if (details.diagnostics.length > 0) {
|
|
28
|
-
lines.push(
|
|
28
|
+
lines.push(" diagnostics");
|
|
29
29
|
for (const diagnostic of details.diagnostics) {
|
|
30
30
|
lines.push(` ${replaceHomeDirectoryWithTilde(diagnostic)}`);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
return lines.join(
|
|
34
|
+
return lines.join("\n");
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
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", "user", "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,9 +28,9 @@ export function groupLibrarySummariesByScope(librarySummaries: ILibrarySummary[]
|
|
|
28
28
|
return orderedLibrarySummariesByScope;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function toDisplayScope(scope: ILibrarySummary[
|
|
32
|
-
if (scope ===
|
|
33
|
-
return
|
|
31
|
+
function toDisplayScope(scope: ILibrarySummary["scope"]): string {
|
|
32
|
+
if (scope === "temporary") {
|
|
33
|
+
return "path";
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
return scope;
|
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,22 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext, InputEventResult } from
|
|
2
|
-
import { Box, Text } from
|
|
3
|
-
import { dirname } from
|
|
4
|
-
import { fileURLToPath } from
|
|
5
|
-
import { createLibraryReport } from
|
|
6
|
-
import { discoverLibrarySkills } from
|
|
7
|
-
import { expandLibrarySkill } from
|
|
8
|
-
import { parseLibraryCommand } from
|
|
9
|
-
import { renderLibraryReport } from
|
|
10
|
-
import type { ILibraryReportDetails, ILibrarySkillDiscovery } from
|
|
11
|
-
|
|
12
|
-
const INFO_COMMAND_NAME =
|
|
13
|
-
const STARTUP_REPORT_MESSAGE_TYPE =
|
|
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, MessageContent } from "./types.js";
|
|
11
|
+
|
|
12
|
+
const INFO_COMMAND_NAME = "pi-skill-library";
|
|
13
|
+
const STARTUP_REPORT_MESSAGE_TYPE = "pi-skill-library.startup-report";
|
|
14
14
|
const extensionPackageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
15
|
-
const handledInputEventResult: InputEventResult = { action:
|
|
15
|
+
const handledInputEventResult: InputEventResult = { action: "handled" };
|
|
16
16
|
|
|
17
17
|
export default function piSkillLibraryExtension(pi: ExtensionAPI): void {
|
|
18
18
|
let cachedLibrarySkillDiscovery: ILibrarySkillDiscovery | null = null;
|
|
19
|
-
let cachedCwd =
|
|
19
|
+
let cachedCwd = "";
|
|
20
20
|
|
|
21
21
|
const ensureLibrarySkillCommandsRegistered = (librarySkillDiscovery: ILibrarySkillDiscovery): void => {
|
|
22
22
|
for (const librarySkill of librarySkillDiscovery.skills) {
|
|
@@ -30,7 +30,7 @@ export default function piSkillLibraryExtension(pi: ExtensionAPI): void {
|
|
|
30
30
|
if (ctx.hasUI) {
|
|
31
31
|
ctx.ui.notify(
|
|
32
32
|
`Library skill is no longer available: ${librarySkill.name}. Run /reload if discovery changed.`,
|
|
33
|
-
|
|
33
|
+
"error",
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
return;
|
|
@@ -42,7 +42,7 @@ export default function piSkillLibraryExtension(pi: ExtensionAPI): void {
|
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
pi.sendUserMessage(expandedText, { deliverAs:
|
|
45
|
+
pi.sendUserMessage(expandedText, { deliverAs: "followUp" });
|
|
46
46
|
},
|
|
47
47
|
});
|
|
48
48
|
}
|
|
@@ -61,7 +61,7 @@ export default function piSkillLibraryExtension(pi: ExtensionAPI): void {
|
|
|
61
61
|
|
|
62
62
|
const invalidateLibrarySkillDiscovery = (): void => {
|
|
63
63
|
cachedLibrarySkillDiscovery = null;
|
|
64
|
-
cachedCwd =
|
|
64
|
+
cachedCwd = "";
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
const onSessionChanged = async (_event: unknown, ctx: ExtensionContext): Promise<void> => {
|
|
@@ -71,15 +71,14 @@ export default function piSkillLibraryExtension(pi: ExtensionAPI): void {
|
|
|
71
71
|
|
|
72
72
|
pi.registerMessageRenderer(STARTUP_REPORT_MESSAGE_TYPE, (message, _options, theme) => {
|
|
73
73
|
const reportDetails = readLibraryReportDetails(message.details);
|
|
74
|
-
const reportText =
|
|
75
|
-
? getMessageTextContent(message.content)
|
|
76
|
-
: renderLibraryReport(theme, reportDetails);
|
|
74
|
+
const reportText =
|
|
75
|
+
reportDetails === null ? getMessageTextContent(message.content) : renderLibraryReport(theme, reportDetails);
|
|
77
76
|
const box = new Box(0, 0);
|
|
78
77
|
box.addChild(new Text(reportText, 0, 0));
|
|
79
78
|
return box;
|
|
80
79
|
});
|
|
81
80
|
|
|
82
|
-
pi.on(
|
|
81
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
83
82
|
invalidateLibrarySkillDiscovery();
|
|
84
83
|
const librarySkillDiscovery = await refreshLibrarySkillDiscovery(ctx.cwd);
|
|
85
84
|
pi.sendMessage({
|
|
@@ -89,12 +88,12 @@ export default function piSkillLibraryExtension(pi: ExtensionAPI): void {
|
|
|
89
88
|
details: createLibraryReportDetails(librarySkillDiscovery),
|
|
90
89
|
});
|
|
91
90
|
});
|
|
92
|
-
pi.on(
|
|
93
|
-
pi.on(
|
|
94
|
-
pi.on(
|
|
91
|
+
pi.on("session_before_switch", onSessionChanged);
|
|
92
|
+
pi.on("session_before_fork", onSessionChanged);
|
|
93
|
+
pi.on("session_before_tree", onSessionChanged);
|
|
95
94
|
|
|
96
95
|
pi.registerCommand(INFO_COMMAND_NAME, {
|
|
97
|
-
description:
|
|
96
|
+
description: "Print the discovered skills-library roots and skills",
|
|
98
97
|
handler: async (_args, ctx) => {
|
|
99
98
|
const librarySkillDiscovery = await refreshLibrarySkillDiscovery(ctx.cwd);
|
|
100
99
|
pi.sendMessage({
|
|
@@ -106,16 +105,16 @@ export default function piSkillLibraryExtension(pi: ExtensionAPI): void {
|
|
|
106
105
|
},
|
|
107
106
|
});
|
|
108
107
|
|
|
109
|
-
pi.on(
|
|
108
|
+
pi.on("context", async (event) => {
|
|
110
109
|
return {
|
|
111
110
|
messages: event.messages.filter((message) => {
|
|
112
|
-
const messageCustomType =
|
|
111
|
+
const messageCustomType = "customType" in message ? message.customType : undefined;
|
|
113
112
|
return messageCustomType !== STARTUP_REPORT_MESSAGE_TYPE;
|
|
114
113
|
}),
|
|
115
114
|
};
|
|
116
115
|
});
|
|
117
116
|
|
|
118
|
-
pi.on(
|
|
117
|
+
pi.on("input", async (event, ctx): Promise<InputEventResult | undefined> => {
|
|
119
118
|
const libraryCommand = parseLibraryCommand(event.text);
|
|
120
119
|
if (libraryCommand === null) {
|
|
121
120
|
return undefined;
|
|
@@ -125,12 +124,13 @@ export default function piSkillLibraryExtension(pi: ExtensionAPI): void {
|
|
|
125
124
|
const skill = librarySkillDiscovery.skillByName.get(libraryCommand.skillName);
|
|
126
125
|
if (skill === undefined) {
|
|
127
126
|
const availableSkillNames = librarySkillDiscovery.skills.map((librarySkill) => librarySkill.name).sort();
|
|
128
|
-
const availableSkillSummary =
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
const availableSkillSummary =
|
|
128
|
+
availableSkillNames.length === 0
|
|
129
|
+
? "No library skills are currently available."
|
|
130
|
+
: `Available library skills: ${availableSkillNames.join(", ")}`;
|
|
131
131
|
|
|
132
132
|
if (ctx.hasUI) {
|
|
133
|
-
ctx.ui.notify(`Unknown library skill: ${libraryCommand.skillName}. ${availableSkillSummary}`,
|
|
133
|
+
ctx.ui.notify(`Unknown library skill: ${libraryCommand.skillName}. ${availableSkillSummary}`, "error");
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
return handledInputEventResult;
|
|
@@ -157,44 +157,48 @@ function readLibraryReportDetails(details: unknown): ILibraryReportDetails | nul
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
function isLibraryReportDetails(value: unknown): value is ILibraryReportDetails {
|
|
160
|
-
if (typeof value !==
|
|
160
|
+
if (typeof value !== "object" || value === null) {
|
|
161
161
|
return false;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
const diagnostics = Reflect.get(value,
|
|
165
|
-
const librarySummaries = Reflect.get(value,
|
|
166
|
-
return
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
164
|
+
const diagnostics = Reflect.get(value, "diagnostics");
|
|
165
|
+
const librarySummaries = Reflect.get(value, "librarySummaries");
|
|
166
|
+
return (
|
|
167
|
+
Array.isArray(diagnostics) &&
|
|
168
|
+
diagnostics.every((diagnostic) => typeof diagnostic === "string") &&
|
|
169
|
+
Array.isArray(librarySummaries) &&
|
|
170
|
+
librarySummaries.every(isLibrarySummary)
|
|
171
|
+
);
|
|
170
172
|
}
|
|
171
173
|
|
|
172
|
-
function isLibrarySummary(value: unknown): value is ILibraryReportDetails[
|
|
173
|
-
if (typeof value !==
|
|
174
|
+
function isLibrarySummary(value: unknown): value is ILibraryReportDetails["librarySummaries"][number] {
|
|
175
|
+
if (typeof value !== "object" || value === null) {
|
|
174
176
|
return false;
|
|
175
177
|
}
|
|
176
178
|
|
|
177
|
-
const libraryPath = Reflect.get(value,
|
|
178
|
-
const scope = Reflect.get(value,
|
|
179
|
-
const skillNames = Reflect.get(value,
|
|
180
|
-
return
|
|
181
|
-
&&
|
|
182
|
-
|
|
183
|
-
|
|
179
|
+
const libraryPath = Reflect.get(value, "libraryPath");
|
|
180
|
+
const scope = Reflect.get(value, "scope");
|
|
181
|
+
const skillNames = Reflect.get(value, "skillNames");
|
|
182
|
+
return (
|
|
183
|
+
typeof libraryPath === "string" &&
|
|
184
|
+
isLibrarySummaryScope(scope) &&
|
|
185
|
+
Array.isArray(skillNames) &&
|
|
186
|
+
skillNames.every((skillName) => typeof skillName === "string")
|
|
187
|
+
);
|
|
184
188
|
}
|
|
185
189
|
|
|
186
|
-
function isLibrarySummaryScope(value: unknown): value is ILibraryReportDetails[
|
|
187
|
-
return value ===
|
|
190
|
+
function isLibrarySummaryScope(value: unknown): value is ILibraryReportDetails["librarySummaries"][number]["scope"] {
|
|
191
|
+
return value === "project" || value === "user" || value === "temporary";
|
|
188
192
|
}
|
|
189
193
|
|
|
190
|
-
function getMessageTextContent(content:
|
|
191
|
-
if (typeof content ===
|
|
194
|
+
function getMessageTextContent(content: MessageContent): string {
|
|
195
|
+
if (typeof content === "string") {
|
|
192
196
|
return content;
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
return content
|
|
196
|
-
.map((part) => (
|
|
197
|
-
.join(
|
|
200
|
+
.map((part) => ("text" in part && typeof part.text === "string" ? part.text : "[non-text content omitted]"))
|
|
201
|
+
.join("\n");
|
|
198
202
|
}
|
|
199
203
|
|
|
200
204
|
function createLibraryCommandName(skillName: string): string {
|
|
@@ -203,7 +207,7 @@ function createLibraryCommandName(skillName: string): string {
|
|
|
203
207
|
|
|
204
208
|
function createTransformInputEventResult(text: string): InputEventResult {
|
|
205
209
|
return {
|
|
206
|
-
action:
|
|
210
|
+
action: "transform",
|
|
207
211
|
text,
|
|
208
212
|
};
|
|
209
213
|
}
|
|
@@ -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", "[Skills 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
|
|
|
@@ -23,3 +23,10 @@ export interface ILibrarySkillDiscovery {
|
|
|
23
23
|
libraryPaths: string[];
|
|
24
24
|
librarySummaries: ILibrarySummary[];
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
export interface IMessageContentPart {
|
|
28
|
+
type: string;
|
|
29
|
+
text?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type MessageContent = string | IMessageContentPart[];
|