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 +25 -12
- package/README.md +12 -2
- package/SECURITY.md +8 -0
- package/extensions/index.ts +2 -0
- package/package.json +5 -5
- package/src/extensions/progressive-skills.ts +55 -0
- package/src/extensions/skill-visibility.ts +113 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
##
|
|
7
|
+
## [Unreleased]
|
|
8
8
|
|
|
9
|
-
## 0.
|
|
9
|
+
## [0.4.0] - 2026-07-28
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
### Added
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
## [
|
|
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.
|
|
31
|
+
## [0.3.12] - 2026-07-13
|
|
28
32
|
|
|
29
33
|
### Fixed
|
|
30
34
|
|
|
31
|
-
-
|
|
32
|
-
|
|
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
|
-
|
|
7
|
+
Make [Pi](https://pi.dev) skills easier to invoke, curate, and control without editing their source files.
|
|
8
8
|
|
|
9
|
-
|
|
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.
|
package/extensions/index.ts
CHANGED
|
@@ -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
|
+
"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.
|
|
49
|
-
"@earendil-works/pi-tui": "^0.80.
|
|
50
|
-
"@types/node": "^26.1.
|
|
51
|
-
"typescript": "^
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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");
|