pi-skillful 0.3.13 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,18 +1,24 @@
1
1
  # Changelog
2
2
 
3
- ## 0.3.13 - 2026-07-17
3
+ All notable changes to this project will be documented in this file.
4
4
 
5
- ## 0.3.12 - 2026-07-12
5
+ This project follows the spirit of [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and uses semantic versioning for releases.
6
6
 
7
- ## 0.3.11 - 2026-07-01
7
+ ## [Unreleased]
8
8
 
9
- ## 0.3.9 - 2026-06-14
9
+ ## [0.4.0] - 2026-07-28
10
10
 
11
- All notable changes to this project will be documented in this file.
11
+ ### Added
12
12
 
13
- This project follows the spirit of [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and uses semantic versioning for releases.
13
+ - Progressive skill loading: discover `.agents/skills/` directories from all ancestor directories (like `AGENTS.md`), not just within the git repo boundary. Uses the `resources_discover` event to contribute missed paths.
14
14
 
15
- ## [Unreleased]
15
+ ## [0.3.14] - 2026-07-17
16
+
17
+ ### Fixed
18
+
19
+ - Restore red/dim coloring for hidden and visible skills in Pi's startup `[Skills]` list while preserving project-trust-aware settings.
20
+
21
+ ## [0.3.13] - 2026-07-17
16
22
 
17
23
  ### Fixed
18
24
 
@@ -20,16 +26,23 @@ This project follows the spirit of [Keep a Changelog](https://keepachangelog.com
20
26
  - Let an explicit project `toggleModifier: "alt"` override a non-default global modifier; unsupported explicit values now follow the documented `"alt"` fallback.
21
27
  - Consume only configured modifier-slot combinations in the prompt editor instead of registering all 54 possible shortcuts.
22
28
  - Propagate focus and custom-editor hooks through the session-toggle editor wrapper for IME and extension compatibility.
23
- - Remove the private `InteractiveMode.prototype` startup skill-list patch; visibility now uses only documented Pi APIs.
24
29
  - Remove the stale package-local `package-lock.json` in favor of the monorepo root lockfile.
25
- - Preserve configured hidden skills at session start. Skillful no longer treats a temporarily incomplete loaded-skill list as authoritative and removes global or project visibility settings.
26
30
 
27
- ## [0.3.10] - 2026-07-01
31
+ ## [0.3.12] - 2026-07-13
28
32
 
29
33
  ### Fixed
30
34
 
31
- - Color hidden skills in the startup `[Skills]` list on `@earendil-works/pi-coding-agent` 0.80+. The 0.80 release moved the loaded-resources container out of `chatContainer`, so the existing prototype patch walked the wrong container and the red/dim colorization no longer applied. The patch now also walks `loadedResourcesContainer`, where 0.80 actually renders the `[Skills]` section.
32
- - Bumped dev dependency range to `^0.80.0` to match the new floor.
35
+ - Preserve configured hidden skills at session start instead of pruning settings from a temporarily incomplete loaded-skill list.
36
+
37
+ ## [0.3.11] - 2026-07-01
38
+
39
+ ### Fixed
40
+
41
+ - Color hidden skills in the startup `[Skills]` list on Pi 0.80+, which moved the rendered resources into `loadedResourcesContainer`.
42
+
43
+ ### Changed
44
+
45
+ - Raise the Pi development dependency floor to `^0.80.0`.
33
46
 
34
47
  ## [0.3.9] - 2026-06-14
35
48
 
package/README.md CHANGED
@@ -4,10 +4,11 @@
4
4
 
5
5
  # pi-skillful
6
6
 
7
- `pi-skillful` is a [Pi](https://pi.dev) package that improves skill workflows.
7
+ Make [Pi](https://pi.dev) skills easier to invoke, curate, and control without editing their source files.
8
8
 
9
- It currently provides three extensions:
9
+ `pi-skillful` provides four focused upgrades:
10
10
 
11
+ - **Progressive skill loading**: discover `.agents/skills/` from every ancestor directory (like `AGENTS.md`), not just within the git repo boundary.
11
12
  - **Inline skill invocation**: invoke one or more skills anywhere in a prompt with `/skill:name`.
12
13
  - **Skill prompt visibility**: choose which skills are hidden from the model's automatic skill-discovery prompt while keeping them explicitly invokable.
13
14
  - **Session skill toggles**: assign up to nine skills to number slots and toggle model visibility while writing a prompt.
@@ -17,6 +18,14 @@ It currently provides three extensions:
17
18
 
18
19
  ## Features
19
20
 
21
+ ### Progressive skill loading
22
+
23
+ Pi discovers `.agents/skills/` directories in ancestor folders, but stops at the git repo root — skills above the repo boundary are invisible. `pi-skillful` removes that boundary by walking the full ancestor chain to the filesystem root, mirroring how `AGENTS.md` context files work.
24
+
25
+ No configuration needed. When a git repo is detected, `pi-skillful` automatically contributes `.agents/skills/` directories from parent directories above the repo root (excluding `~/.agents/skills/`, which Pi already loads globally). In projects without a git repo, Pi already walks to the filesystem root and this feature has no effect.
26
+
27
+ Skill name collisions follow Pi's first-wins rule: skills discovered closer to the working directory take precedence over skills with the same name found higher up.
28
+
20
29
  ### Inline skill invocation
21
30
 
22
31
  Vanilla Pi expands `/skill:name` only when it appears at the beginning of the prompt. `pi-skillful` expands known skill markers anywhere in the prompt, including multiple skills:
@@ -33,6 +42,7 @@ Hide skills from the `<available_skills>` section of the system prompt without e
33
42
 
34
43
  Hidden skills:
35
44
 
45
+ - appear in the startup `[Skills]` list in the theme's error color, while visible skills appear dimmed;
36
46
  - are not advertised to the model for automatic skill selection;
37
47
  - remain loaded by Pi;
38
48
  - remain available for explicit invocation with `/skill:name`, including inline invocation.
package/SECURITY.md CHANGED
@@ -20,3 +20,11 @@ The maintainer will acknowledge reports as soon as practical and coordinate disc
20
20
  ## Security model
21
21
 
22
22
  `pi-skillful` is a Pi package. Pi extensions execute with the same permissions as the local user running Pi. Users should review installed Pi packages and only install packages from sources they trust.
23
+
24
+ ### Progressive skill loading and trust boundary
25
+
26
+ `pi-skillful` extends Pi's skill discovery to ancestor directories above the git repository root, loading `.agents/skills/` from every parent directory up to the filesystem root (excluding `~/.agents/skills/`, which Pi already loads globally).
27
+
28
+ This means skills placed in a shared or writable ancestor directory — for example, `/home/shared/.agents/skills/` or `/tmp/.agents/skills/` — are automatically available as skill instructions when Pi runs in any nested repository. Review the contents and ownership of `.agents/skills/` directories above your repositories, as they are now part of the effective skill set.
29
+
30
+ To disable progressive loading without removing the package, uninstall `pi-skillful` or use Pi's built-in `--no-skills` flag to suppress all skill loading for a session.
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import inlineSkillInvocation from "../src/extensions/inline-skill-invocation.js";
3
+ import progressiveSkills from "../src/extensions/progressive-skills.js";
3
4
  import { reportInstallTelemetry } from "../src/install-telemetry.js";
4
5
  import skillVisibility from "../src/extensions/skill-visibility.js";
5
6
  import sessionSkillToggles from "../src/extensions/session-skill-toggles.js";
@@ -7,6 +8,7 @@ import sessionSkillToggles from "../src/extensions/session-skill-toggles.js";
7
8
  export default function piSkillful(pi: ExtensionAPI) {
8
9
  reportInstallTelemetry();
9
10
 
11
+ progressiveSkills(pi);
10
12
  inlineSkillInvocation(pi);
11
13
  skillVisibility(pi);
12
14
  sessionSkillToggles(pi);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-skillful",
3
- "version": "0.3.13",
3
+ "version": "0.4.0",
4
4
  "description": "Pi package with skill invocation and visibility improvements.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -45,10 +45,10 @@
45
45
  "@earendil-works/pi-tui": "*"
46
46
  },
47
47
  "devDependencies": {
48
- "@earendil-works/pi-coding-agent": "^0.80.0",
49
- "@earendil-works/pi-tui": "^0.80.0",
50
- "@types/node": "^26.1.0",
51
- "typescript": "^6.0.3"
48
+ "@earendil-works/pi-coding-agent": "^0.80.10",
49
+ "@earendil-works/pi-tui": "^0.80.10",
50
+ "@types/node": "^26.1.1",
51
+ "typescript": "^7.0.2"
52
52
  },
53
53
  "scripts": {
54
54
  "check": "tsc --noEmit",
@@ -0,0 +1,55 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
+
6
+ /**
7
+ * Walks from cwd up through all ancestor directories (no git boundary)
8
+ * and discovers `.agents/skills/` directories that Pi's built-in discovery
9
+ * misses because it stops at the git repo root.
10
+ *
11
+ * This mirrors how AGENTS.md context files are loaded from every parent
12
+ * directory, not just within the repo boundary.
13
+ */
14
+ export default function progressiveSkills(pi: ExtensionAPI) {
15
+ pi.on("resources_discover", async (event) => {
16
+ const gitRoot = findGitRoot(event.cwd);
17
+
18
+ // No git repo — Pi already walks to filesystem root, nothing to add.
19
+ if (!gitRoot) return;
20
+
21
+ const resolvedGitRoot = resolve(gitRoot);
22
+ const homeAgentsSkills = resolve(homedir(), ".agents", "skills");
23
+ const skillPaths: string[] = [];
24
+
25
+ // Walk from git root's parent up to filesystem root.
26
+ // Order: git-parent first (closer to cwd = higher priority via first-wins).
27
+ let dir = dirname(resolvedGitRoot);
28
+ while (true) {
29
+ const agentsSkills = join(dir, ".agents", "skills");
30
+ const resolved = resolve(agentsSkills);
31
+
32
+ // Skip ~/.agents/skills (handled globally by Pi) and non-existent dirs.
33
+ if (resolved !== homeAgentsSkills && existsSync(resolved)) {
34
+ skillPaths.push(resolved);
35
+ }
36
+
37
+ const parent = dirname(dir);
38
+ if (parent === dir) break;
39
+ dir = parent;
40
+ }
41
+
42
+ if (skillPaths.length === 0) return;
43
+ return { skillPaths };
44
+ });
45
+ }
46
+
47
+ function findGitRoot(startDir: string): string | undefined {
48
+ let dir = resolve(startDir);
49
+ while (true) {
50
+ if (existsSync(join(dir, ".git"))) return dir;
51
+ const parent = dirname(dir);
52
+ if (parent === dir) return undefined;
53
+ dir = parent;
54
+ }
55
+ }
@@ -1,6 +1,14 @@
1
- import { DynamicBorder, getSettingsListTheme, type ExtensionAPI, type Skill, type Theme } from "@earendil-works/pi-coding-agent";
1
+ import {
2
+ DynamicBorder,
3
+ getSettingsListTheme,
4
+ InteractiveMode,
5
+ type ExtensionAPI,
6
+ type Skill,
7
+ type Theme,
8
+ } from "@earendil-works/pi-coding-agent";
2
9
  import { type Component, Key, matchesKey, type SettingItem, SettingsList, truncateToWidth, type TUI } from "@earendil-works/pi-tui";
3
10
  import {
11
+ normalizeSkillName,
4
12
  normalizeSkillNames,
5
13
  readEffectiveHiddenSkills,
6
14
  readScopedSkillfulSettings,
@@ -15,6 +23,34 @@ import { replaceSkillsSection } from "../skill-prompt.js";
15
23
  import { isTopLevelSkill, listLoadedSkills, type LoadedSkillInfo } from "../skills.js";
16
24
  import { hasActiveSessionSkillToggles, refreshSessionSkillToggles } from "./session-skill-toggles.js";
17
25
  const SCOPES: SkillfulScope[] = ["global", "project"];
26
+ const STORE_KEY = Symbol.for("pi-skillful.skillVisibilityStore");
27
+ const STARTUP_PATCH_KEY = Symbol.for("pi-skillful.startupPatchV3");
28
+
29
+ interface SkillVisibilityStore {
30
+ hiddenSkillsByCwd: Map<string, Set<string>>;
31
+ theme: Theme | null;
32
+ }
33
+
34
+ interface ExpandableTextLike {
35
+ getCollapsedText: () => string;
36
+ setText: (text: string) => void;
37
+ }
38
+
39
+ interface BoxLike {
40
+ children: unknown[];
41
+ }
42
+
43
+ interface InteractiveModeLike {
44
+ loadedResourcesContainer?: BoxLike;
45
+ showLoadedResources?: (options?: unknown) => void;
46
+ session?: { resourceLoader?: { getSkills: () => { skills: Skill[]; diagnostics: unknown[] } } };
47
+ sessionManager?: { getCwd?: () => string };
48
+ }
49
+
50
+ const store = (((globalThis as Record<PropertyKey, unknown>)[STORE_KEY] as SkillVisibilityStore | undefined) ??= {
51
+ hiddenSkillsByCwd: new Map<string, Set<string>>(),
52
+ theme: null,
53
+ }) as SkillVisibilityStore;
18
54
 
19
55
  type SkillListItem = LoadedSkillInfo;
20
56
  type HiddenSkillsByScope = Record<SkillfulScope, Set<string>>;
@@ -44,14 +80,17 @@ interface SettingsListSelectionView {
44
80
  }
45
81
 
46
82
  export default function skillVisibility(pi: ExtensionAPI) {
83
+ installStartupSkillListPatch();
84
+
47
85
  pi.on("session_start", async (_event, ctx) => {
48
- await readEffectiveHiddenSkills(ctx.cwd, ctx.isProjectTrusted());
86
+ store.theme = ctx.ui.theme;
87
+ await refreshHiddenSkillCache(ctx.cwd, ctx.isProjectTrusted());
49
88
  });
50
89
 
51
90
  pi.on("before_agent_start", async (event, ctx) => {
52
91
  if (hasActiveSessionSkillToggles()) return;
53
92
 
54
- const hidden = await readEffectiveHiddenSkills(ctx.cwd, ctx.isProjectTrusted());
93
+ const hidden = await refreshHiddenSkillCache(ctx.cwd, ctx.isProjectTrusted());
55
94
  if (hidden.size === 0 || !event.systemPromptOptions.skills?.length) return;
56
95
 
57
96
  const filteredSkills: Skill[] = event.systemPromptOptions.skills.map((skill) =>
@@ -110,6 +149,76 @@ export default function skillVisibility(pi: ExtensionAPI) {
110
149
  });
111
150
  }
112
151
 
152
+ async function refreshHiddenSkillCache(cwd: string, projectTrusted: boolean): Promise<Set<string>> {
153
+ const hidden = await readEffectiveHiddenSkills(cwd, projectTrusted);
154
+ store.hiddenSkillsByCwd.set(cwd, hidden);
155
+ return hidden;
156
+ }
157
+
158
+ // Pi has no public hook for styling the built-in startup resource list, so patch the
159
+ // prototype from Pi's own module and keep the workaround isolated here.
160
+ export function installStartupSkillListPatch(
161
+ realPrototype: InteractiveModeLike = (InteractiveMode as unknown as { prototype: InteractiveModeLike }).prototype,
162
+ ): void {
163
+ if (!realPrototype) return;
164
+
165
+ const patchState = realPrototype as Record<PropertyKey, unknown>;
166
+ if (patchState[STARTUP_PATCH_KEY]) return;
167
+
168
+ const original = realPrototype.showLoadedResources;
169
+ if (typeof original !== "function") return;
170
+
171
+ realPrototype.showLoadedResources = function (this: InteractiveModeLike, options?: unknown): void {
172
+ const loader = this.session?.resourceLoader;
173
+ const originalGetSkills = loader?.getSkills;
174
+ if (!loader || typeof originalGetSkills !== "function") {
175
+ original.call(this, options);
176
+ return;
177
+ }
178
+
179
+ const cwd = this.sessionManager?.getCwd?.();
180
+ let rawSkillNames: string[] = [];
181
+ loader.getSkills = () => {
182
+ const result = originalGetSkills.call(loader);
183
+ rawSkillNames = result.skills.map((skill) => normalizeSkillName(skill.name));
184
+ return result;
185
+ };
186
+
187
+ const childrenBefore = this.loadedResourcesContainer?.children.length ?? 0;
188
+ try {
189
+ original.call(this, options);
190
+ } finally {
191
+ loader.getSkills = originalGetSkills;
192
+ }
193
+
194
+ if (rawSkillNames.length === 0 || !cwd || !this.loadedResourcesContainer) return;
195
+
196
+ const hidden = store.hiddenSkillsByCwd.get(cwd) ?? new Set<string>();
197
+ const children = this.loadedResourcesContainer.children;
198
+ for (let index = childrenBefore; index < children.length; index++) {
199
+ const child = children[index] as ExpandableTextLike | undefined;
200
+ if (!child || typeof child.getCollapsedText !== "function") continue;
201
+ if (!child.getCollapsedText().includes("[Skills]")) continue;
202
+
203
+ const colorized = buildColorizedSkillList(rawSkillNames, hidden, store.theme);
204
+ child.getCollapsedText = () => colorized;
205
+ child.setText(colorized);
206
+ break;
207
+ }
208
+ };
209
+
210
+ patchState[STARTUP_PATCH_KEY] = true;
211
+ }
212
+
213
+ export function buildColorizedSkillList(names: string[], hidden: Set<string>, theme: Theme | null): string {
214
+ const sorted = [...names].sort((a, b) => a.localeCompare(b));
215
+ if (!theme) return `[Skills]\n ${sorted.join(", ")}`;
216
+
217
+ const header = theme.fg("mdHeading", "[Skills]");
218
+ const parts = sorted.map((name) => (hidden.has(name) ? theme.fg("error", name) : theme.fg("dim", name)));
219
+ return `${header}\n ${parts.join(", ")}`;
220
+ }
221
+
113
222
  function getSkillItems(pi: ExtensionAPI): SkillListItem[] {
114
223
  return listLoadedSkills(pi.getCommands()).filter(isTopLevelSkill);
115
224
  }
@@ -331,7 +440,7 @@ class SkillfulVisibilityMenu implements Component {
331
440
  .then(async () => {
332
441
  if (scope === "project") await this.writeProjectOverrideOrInheritance();
333
442
  else await writeHiddenSkills(scope, this.cwd, hiddenSnapshot);
334
- await readEffectiveHiddenSkills(this.cwd, this.projectTrusted);
443
+ await refreshHiddenSkillCache(this.cwd, this.projectTrusted);
335
444
  })
336
445
  .catch((error) => {
337
446
  this.notify(`Failed to save ${scope} skill visibility: ${error instanceof Error ? error.message : String(error)}`, "error");