@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 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. Home-directory paths are rendered with the `~/` convention.
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.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
- "@mariozechner/pi-coding-agent": "0.64.0",
42
- "@types/node": "24.7.2",
43
- "@typescript/native-preview": "7.0.0-dev.20260330.1",
44
- "dprint": "0.53.0",
45
- "oxlint": "1.56.0",
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
- "dev": "bun x pi -e ./src/index.ts",
50
- "check": "bun run typecheck && bun run lint && bun run format:check && bun run verify:pi-load",
51
- "typecheck": "tsgo --noEmit -p tsconfig.json",
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 './groupLibrarySummariesByScope.js';
2
- import { replaceHomeDirectoryWithTilde } from './replaceHomeDirectoryWithTilde.js';
3
- import type { ILibraryReportDetails, ILibrarySkillDiscovery, ILibrarySummary } from './types.js';
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(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]'];
5
+ export function createLibraryReport(details: ILibraryReportDetails): string {
6
+ const lines = ["[@alexgorbatchev/pi-skill-library]"];
14
7
  if (details.librarySummaries.length === 0) {
15
- lines.push(' No library skills were discovered.');
16
- return lines.join('\n');
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(' diagnostics');
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('\n');
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 '@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';
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 = '@alexgorbatchev/pi-skills-library';
15
- const LIBRARY_DIRECTORY_NAME = 'skills-library';
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 = 'project' | 'user' | 'temporary';
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, '.pi');
35
+ const projectSettingsBaseDir = path.join(cwd, ".pi");
36
36
  const userSettingsBaseDir = getAgentDir();
37
37
 
38
- const projectConfiguredPaths = getConfiguredLibraryPaths(projectSettings, projectSettingsBaseDir, 'project');
39
- const userConfiguredPaths = getConfiguredLibraryPaths(userSettings, userSettingsBaseDir, 'user');
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
- 'project',
43
+ "project",
44
44
  );
45
45
  const userConfiguredSkillSiblingPaths = getConfiguredSkillSiblingLibraryPaths(
46
46
  userSettings,
47
47
  userSettingsBaseDir,
48
- 'user',
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, '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'),
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, 'skills'))
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, '.pi', LIBRARY_DIRECTORY_NAME),
127
- ...ancestorDirectories.map((directoryPath) => path.join(directoryPath, '.agents', LIBRARY_DIRECTORY_NAME)),
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(), '.agents', LIBRARY_DIRECTORY_NAME),
131
+ path.join(homedir(), ".agents", LIBRARY_DIRECTORY_NAME),
132
132
  ];
133
133
 
134
134
  return [
135
- ...projectPaths.map(
136
- (libraryPath): ILibraryPathCandidate => ({ path: libraryPath, scope: 'project' }),
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, '.pi');
182
+ const projectPiRoot = path.resolve(cwd, ".pi");
187
183
 
188
184
  if (
189
- isPathInside(normalizedPackageRoot, normalizedProjectRoot) || isPathInside(normalizedPackageRoot, projectPiRoot)
185
+ isPathInside(normalizedPackageRoot, normalizedProjectRoot) ||
186
+ isPathInside(normalizedPackageRoot, projectPiRoot)
190
187
  ) {
191
- return 'project';
188
+ return "project";
192
189
  }
193
190
 
194
- return 'user';
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 !== 'package' || skill.sourceInfo.baseDir === undefined) {
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, '.git'))) {
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('skills');
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 === '' || (!relativePath.startsWith('..') && !path.isAbsolute(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 === 'object' && value !== null;
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 !== 'string') {
349
+ if (typeof entry !== "string") {
353
350
  continue;
354
351
  }
355
352
 
@@ -1,10 +1,9 @@
1
- import { type Skill, stripFrontmatter } from '@mariozechner/pi-coding-agent';
2
- import { readFile } from 'node:fs/promises';
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, 'utf8');
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 './types.js';
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 = ['project', 'user', 'path'];
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['scope']): string {
32
- if (scope === 'temporary') {
33
- return 'path';
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 './piSkillLibraryExtension.js';
1
+ export { default } from "./piSkillLibraryExtension.js";
@@ -1,6 +1,6 @@
1
- import type { ILibraryCommand } from './types.js';
1
+ import type { ILibraryCommand } from "./types.js";
2
2
 
3
- const LIBRARY_COMMAND_PREFIX = '/library:';
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 ? '' : commandBody.slice(spaceIndex + 1).trim();
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 { 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';
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: 'handled' };
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
- ctx.ui.notify(
68
+ sendLibraryMessage(
32
69
  `Library skill is no longer available: ${librarySkill.name}. Run /reload if discovery changed.`,
33
- 'error',
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: 'followUp' });
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.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) => {
109
+ pi.on("session_start", async (_event, ctx) => {
83
110
  invalidateLibrarySkillDiscovery();
84
111
  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
- });
112
+
113
+ if (ctx.hasUI) {
114
+ sendLibraryReport(createLibraryReportDetails(librarySkillDiscovery));
115
+ }
91
116
  });
92
- pi.on('session_switch', onSessionChanged);
93
- pi.on('session_fork', onSessionChanged);
94
- pi.on('session_tree', onSessionChanged);
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: 'Print the discovered skills-library roots and skills',
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
- pi.sendMessage({
101
- customType: STARTUP_REPORT_MESSAGE_TYPE,
102
- content: createLibraryReport(librarySkillDiscovery),
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('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> => {
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 = availableSkillNames.length === 0
129
- ? 'No library skills are currently available.'
130
- : `Available library skills: ${availableSkillNames.join(', ')}`;
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
- ctx.ui.notify(`Unknown library skill: ${libraryCommand.skillName}. ${availableSkillSummary}`, 'error');
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 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;
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
- 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';
172
+ return content;
188
173
  }
189
174
 
190
- function getMessageTextContent(content: string | Array<{ type: string; text?: string; }>): string {
191
- if (typeof content === 'string') {
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) => ('text' in part && typeof part.text === 'string' ? part.text : '[non-text content omitted]'))
197
- .join('\n');
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: 'transform',
191
+ action: "transform",
207
192
  text,
208
193
  };
209
194
  }
@@ -1,32 +1,32 @@
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';
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('mdHeading', '[Skills Library]')];
7
+ const lines: string[] = [theme.fg("mdHeading", "[@alexgorbatchev/pi-skill-library]")];
8
8
  if (details.librarySummaries.length === 0) {
9
- lines.push(theme.fg('dim', ' No library skills were discovered.'));
10
- return lines.join('\n');
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('accent', scope)}`);
15
+ lines.push(` ${theme.fg("accent", scope)}`);
16
16
  for (const librarySummary of librarySummaries) {
17
- lines.push(theme.fg('dim', ` ${replaceHomeDirectoryWithTilde(librarySummary.libraryPath)}`));
17
+ lines.push(theme.fg("dim", ` ${replaceHomeDirectoryWithTilde(librarySummary.libraryPath)}`));
18
18
  for (const skillName of librarySummary.skillNames) {
19
- lines.push(theme.fg('dim', ` /library:${skillName}`));
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('warning', 'diagnostics')}`);
25
+ lines.push(` ${theme.fg("warning", "diagnostics")}`);
26
26
  for (const diagnostic of details.diagnostics) {
27
- lines.push(theme.fg('warning', ` ${replaceHomeDirectoryWithTilde(diagnostic)}`));
27
+ lines.push(theme.fg("warning", ` ${replaceHomeDirectoryWithTilde(diagnostic)}`));
28
28
  }
29
29
  }
30
30
 
31
- return lines.join('\n');
31
+ return lines.join("\n");
32
32
  }
@@ -1,5 +1,5 @@
1
- import { homedir } from 'node:os';
2
- import path from 'node:path';
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 '@mariozechner/pi-coding-agent';
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: 'project' | 'user' | 'temporary';
10
+ scope: "project" | "user" | "temporary";
11
11
  skillNames: string[];
12
12
  }
13
13