pi-skillful 0.1.0 → 0.2.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/CHANGELOG.md CHANGED
@@ -6,9 +6,11 @@ This project follows the spirit of [Keep a Changelog](https://keepachangelog.com
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.0] - 2026-05-07
10
+
9
11
  ### Added
10
12
 
11
13
  - Inline `/skill:name` expansion anywhere in a prompt.
12
14
  - `/skillful` menu for global/project skill prompt visibility.
13
15
  - `skillful.hiddenSkills` settings support.
14
- - Startup skill-list annotations for visible vs hidden skills.
16
+ - Startup `[Skills]` list highlights hidden skills in the error color.
package/README.md CHANGED
@@ -57,7 +57,7 @@ Open the menu with:
57
57
 
58
58
  The menu lists all loaded skills alphabetically. Toggle a skill off to save it in the active scope's `hiddenSkills` list. Use the Global/Project tabs to choose which settings file to edit.
59
59
 
60
- Pi's startup `[Skills]` list is also annotated: visible skills are marked with a green dot, and hidden skills are marked with a dim red dot.
60
+ Pi's startup `[Skills]` list also highlights hidden skills in the error color (red in the default dark theme).
61
61
 
62
62
  When the project settings file contains only `skillful` settings and the project `hiddenSkills` list becomes empty, `.pi/settings.json` is deleted instead of leaving an empty settings file behind.
63
63
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-skillful",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Pi package with skill invocation and visibility improvements.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,16 +20,34 @@ import {
20
20
  const SKILLS_SECTION_PATTERN = /\n\nThe following skills provide specialized instructions for specific tasks\.[\s\S]*?<\/available_skills>/;
21
21
  const SCOPES: SkillfulScope[] = ["global", "project"];
22
22
  const STORE_KEY = Symbol.for("pi-skillful.skillVisibilityStore");
23
- const STARTUP_SKILL_LIST_PATCH_KEY = Symbol.for("pi-skillful.startupSkillListPatchInstalled");
23
+ const STARTUP_PATCH_KEY = Symbol.for("pi-skillful.startupPatchV2");
24
24
 
25
25
  interface SkillVisibilityStore {
26
26
  hiddenSkillsByCwd: Map<string, Set<string>>;
27
27
  lastHiddenSkills: Set<string>;
28
+ theme: Theme | null;
28
29
  }
29
30
 
30
- const skillVisibilityStore = (((globalThis as Record<PropertyKey, unknown>)[STORE_KEY] as SkillVisibilityStore | undefined) ??= {
31
+ interface ExpandableTextLike {
32
+ getCollapsedText: () => string;
33
+ setText: (text: string) => void;
34
+ }
35
+
36
+ interface BoxLike {
37
+ children: unknown[];
38
+ }
39
+
40
+ interface InteractiveModeLike {
41
+ chatContainer?: BoxLike;
42
+ showLoadedResources?: (options?: unknown) => void;
43
+ session?: { resourceLoader?: { getSkills: () => { skills: Skill[]; diagnostics: unknown[] } } };
44
+ sessionManager?: { getCwd?: () => string };
45
+ }
46
+
47
+ const store = (((globalThis as Record<PropertyKey, unknown>)[STORE_KEY] as SkillVisibilityStore | undefined) ??= {
31
48
  hiddenSkillsByCwd: new Map<string, Set<string>>(),
32
49
  lastHiddenSkills: new Set<string>(),
50
+ theme: null,
33
51
  }) as SkillVisibilityStore;
34
52
 
35
53
  interface SkillListItem {
@@ -41,7 +59,8 @@ export default function skillVisibility(pi: ExtensionAPI) {
41
59
  installStartupSkillListPatch();
42
60
 
43
61
  pi.on("session_start", async (_event, ctx) => {
44
- await refreshHiddenSkillCache(ctx.cwd);
62
+ store.theme = ctx.ui.theme;
63
+ await pruneStaleHiddenSkills(pi, ctx.cwd);
45
64
  });
46
65
 
47
66
  pi.on("before_agent_start", async (event, ctx) => {
@@ -90,34 +109,45 @@ export default function skillVisibility(pi: ExtensionAPI) {
90
109
  });
91
110
  }
92
111
 
112
+ async function pruneStaleHiddenSkills(pi: ExtensionAPI, cwd: string): Promise<void> {
113
+ const installedNames = new Set(getSkillItems(pi).map((s) => s.name));
114
+ const scoped = await readScopedSkillfulSettings(cwd);
115
+
116
+ await Promise.all(
117
+ SCOPES.map(async (scope) => {
118
+ const current = scoped[scope].hiddenSkills;
119
+ const pruned = current.filter((name) => installedNames.has(name));
120
+ if (pruned.length < current.length) {
121
+ await writeHiddenSkills(scope, cwd, pruned);
122
+ scoped[scope] = { hiddenSkills: pruned };
123
+ }
124
+ }),
125
+ );
126
+
127
+ const hidden = new Set([...scoped.global.hiddenSkills, ...scoped.project.hiddenSkills]);
128
+ store.hiddenSkillsByCwd.set(cwd, hidden);
129
+ store.lastHiddenSkills = hidden;
130
+ }
131
+
93
132
  async function refreshHiddenSkillCache(cwd: string): Promise<Set<string>> {
94
133
  const hidden = await readEffectiveHiddenSkills(cwd);
95
- skillVisibilityStore.hiddenSkillsByCwd.set(cwd, hidden);
96
- skillVisibilityStore.lastHiddenSkills = hidden;
134
+ store.hiddenSkillsByCwd.set(cwd, hidden);
135
+ store.lastHiddenSkills = hidden;
97
136
  return hidden;
98
137
  }
99
138
 
139
+ // Must patch the real prototype from pi's module — separate module resolutions have distinct class identities.
100
140
  function installStartupSkillListPatch(): void {
101
- type StartupResourceLoader = {
102
- getSkills: () => { skills: Skill[]; diagnostics: unknown[] };
103
- };
104
- type InteractiveModeWithStartupResources = {
105
- showLoadedResources?: (options?: unknown) => void;
106
- session?: { resourceLoader?: StartupResourceLoader };
107
- sessionManager?: { getCwd?: () => string };
108
- };
141
+ const realPrototype = (InteractiveMode as unknown as { prototype: InteractiveModeLike }).prototype;
142
+ if (!realPrototype) return;
109
143
 
110
- const prototype = (InteractiveMode as unknown as { prototype: InteractiveModeWithStartupResources }).prototype;
111
- const patchState = prototype as InteractiveModeWithStartupResources & Record<PropertyKey, unknown>;
112
- if (patchState[STARTUP_SKILL_LIST_PATCH_KEY]) return;
144
+ const patchState = realPrototype as Record<PropertyKey, unknown>;
145
+ if (patchState[STARTUP_PATCH_KEY]) return;
113
146
 
114
- const original = prototype.showLoadedResources;
147
+ const original = realPrototype.showLoadedResources;
115
148
  if (typeof original !== "function") return;
116
149
 
117
- prototype.showLoadedResources = function showLoadedResourcesWithSkillfulVisibility(
118
- this: InteractiveModeWithStartupResources,
119
- options?: unknown,
120
- ): void {
150
+ realPrototype.showLoadedResources = function (this: InteractiveModeLike, options?: unknown): void {
121
151
  const loader = this.session?.resourceLoader;
122
152
  const originalGetSkills = loader?.getSkills;
123
153
  if (!loader || typeof originalGetSkills !== "function") {
@@ -126,31 +156,48 @@ function installStartupSkillListPatch(): void {
126
156
  }
127
157
 
128
158
  const cwd = this.sessionManager?.getCwd?.();
129
- const hidden = (cwd ? skillVisibilityStore.hiddenSkillsByCwd.get(cwd) : undefined) ?? skillVisibilityStore.lastHiddenSkills;
130
159
 
160
+ let rawSkillNames: string[] = [];
131
161
  loader.getSkills = () => {
132
162
  const result = originalGetSkills.call(loader);
133
- return {
134
- ...result,
135
- skills: result.skills.map((skill) => ({
136
- ...skill,
137
- name: formatStartupSkillName(skill.name, hidden.has(skill.name)),
138
- })),
139
- };
163
+ rawSkillNames = result.skills.map((s) => normalizeSkillName(s.name));
164
+ return result;
140
165
  };
141
166
 
167
+ const childrenBefore = this.chatContainer?.children.length ?? 0;
168
+
142
169
  try {
143
170
  original.call(this, options);
144
171
  } finally {
145
172
  loader.getSkills = originalGetSkills;
146
173
  }
174
+
175
+ if (rawSkillNames.length === 0 || !cwd || !this.chatContainer) return;
176
+
177
+ const children = this.chatContainer.children;
178
+ for (let i = childrenBefore; i < children.length; i++) {
179
+ const child = children[i] as ExpandableTextLike | undefined;
180
+ if (!child || typeof child.getCollapsedText !== "function") continue;
181
+ const collapsed = child.getCollapsedText();
182
+ if (!collapsed.includes("[Skills]")) continue;
183
+
184
+ child.getCollapsedText = () => buildColorizedSkillList(rawSkillNames, store.lastHiddenSkills, store.theme);
185
+ child.setText(child.getCollapsedText());
186
+ break;
187
+ }
147
188
  };
148
189
 
149
- patchState[STARTUP_SKILL_LIST_PATCH_KEY] = true;
190
+ patchState[STARTUP_PATCH_KEY] = true;
150
191
  }
151
192
 
152
- function formatStartupSkillName(name: string, hidden: boolean): string {
153
- return hidden ? `${name} \x1b[31;2m●\x1b[22;39m` : `${name} \x1b[32m●\x1b[39m`;
193
+ function buildColorizedSkillList(names: string[], hidden: Set<string>, theme: Theme | null): string {
194
+ const sorted = [...names].sort((a, b) => a.localeCompare(b));
195
+ if (!theme) {
196
+ return `[Skills]\n ${sorted.join(", ")}`;
197
+ }
198
+ const header = theme.fg("mdHeading", "[Skills]");
199
+ const parts = sorted.map((n) => (hidden.has(n) ? theme.fg("error", n) : theme.fg("dim", n)));
200
+ return `${header}\n ${parts.join(", ")}`;
154
201
  }
155
202
 
156
203
  function getSkillItems(pi: ExtensionAPI): SkillListItem[] {