@finngi/pi-resources 0.0.0-stage → 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +107 -2
- package/node_modules/pi-resources/LICENSE +21 -0
- package/node_modules/pi-resources/README.md +101 -0
- package/node_modules/pi-resources/dist/extensions/resources.d.ts +3 -0
- package/node_modules/pi-resources/dist/extensions/resources.js +273 -0
- package/node_modules/pi-resources/dist/lib/package-picker.d.ts +3 -0
- package/node_modules/pi-resources/dist/lib/package-picker.js +61 -0
- package/node_modules/pi-resources/dist/lib/picker.d.ts +25 -0
- package/node_modules/pi-resources/dist/lib/picker.js +85 -0
- package/node_modules/pi-resources/dist/lib/resource-packages.d.ts +29 -0
- package/node_modules/pi-resources/dist/lib/resource-packages.js +187 -0
- package/node_modules/pi-resources/dist/lib/resource-selection.d.ts +91 -0
- package/node_modules/pi-resources/dist/lib/resource-selection.js +487 -0
- package/node_modules/pi-resources/dist/lib/saved-instruction-context.d.ts +4 -0
- package/node_modules/pi-resources/dist/lib/saved-instruction-context.js +24 -0
- package/node_modules/pi-resources/dist/lib/session-setup.d.ts +10 -0
- package/node_modules/pi-resources/dist/lib/session-setup.js +561 -0
- package/node_modules/pi-resources/node_modules/pi-contracts/LICENSE +21 -0
- package/node_modules/pi-resources/node_modules/pi-contracts/README.md +16 -0
- package/node_modules/pi-resources/node_modules/pi-contracts/dist/index.d.ts +268 -0
- package/node_modules/pi-resources/node_modules/pi-contracts/dist/index.js +9 -0
- package/node_modules/pi-resources/node_modules/pi-contracts/package.json +31 -0
- package/node_modules/pi-resources/node_modules/pi-resource-browser/LICENSE +21 -0
- package/node_modules/pi-resources/node_modules/pi-resource-browser/README.md +81 -0
- package/node_modules/pi-resources/node_modules/pi-resource-browser/dist/resource-browser.d.ts +130 -0
- package/node_modules/pi-resources/node_modules/pi-resource-browser/dist/resource-browser.js +974 -0
- package/node_modules/pi-resources/node_modules/pi-resource-browser/dist/resource-search.d.ts +14 -0
- package/node_modules/pi-resources/node_modules/pi-resource-browser/dist/resource-search.js +54 -0
- package/node_modules/pi-resources/node_modules/pi-resource-browser/package.json +38 -0
- package/node_modules/pi-resources/package.json +72 -0
- package/package.json +38 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 finngi2 contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,108 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @finngi/pi-resources
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Experimental alpha; tested with Node 24 and stock Pi 1.2.0.
|
|
4
|
+
|
|
5
|
+
`pi install npm:@finngi/pi-resources@0.1.0-alpha.0`
|
|
6
|
+
|
|
7
|
+
This download includes its workspace dependencies. No sibling packages are required.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
Choose which skills and packages a Pi session can use, without loading delegation, background
|
|
11
|
+
jobs or permissions. It adds `/skills`, `/packages` and the shared `session_setup` tool, using
|
|
12
|
+
Pi's loaded resources and the shared resource browser.
|
|
13
|
+
|
|
14
|
+
- `/skills` chooses which skill descriptions the parent's prompt lists. Skill bodies still load on
|
|
15
|
+
demand, and `/skill:name` is refused for skills that are off.
|
|
16
|
+
- `/packages` makes package resources available or hides them. It does not select their skills,
|
|
17
|
+
agents or instructions. Those choices stay independent, and return when a disabled package is
|
|
18
|
+
enabled again. Packages with only extensions are not listed: availability never changes which
|
|
19
|
+
extension code loads. Agents need `pi-delegation` and instructions need `pi-context`. Opening
|
|
20
|
+
the picker loads neither.
|
|
21
|
+
|
|
22
|
+
## Saving choices
|
|
23
|
+
|
|
24
|
+
Session, Project and Global each show their own Inherit, On or Off choice. The most specific
|
|
25
|
+
explicit choice wins; Inherit follows the broader scope, ending at Pi's default. Turning a skill
|
|
26
|
+
off for this session leaves its project and global choices intact. The picker shows the effective
|
|
27
|
+
choice and where it came from separately.
|
|
28
|
+
|
|
29
|
+
Project and Global also offer a session type: **Any session**, **Interactive only** or
|
|
30
|
+
**Type: Inherit**. Interactive means Pi's TUI; RPC (even with dialogs), print mode and delegated
|
|
31
|
+
children are excluded. Session type does not turn a resource on or off. A project type overrides
|
|
32
|
+
the global one. Session has no type of its own.
|
|
33
|
+
|
|
34
|
+
Ctrl+S copies eligible current On/Off selections to project defaults; Ctrl+Shift+S copies them to
|
|
35
|
+
personal defaults. Bulk saves leave session types alone. Changing a single scope control only
|
|
36
|
+
writes that resource at that scope. Editing defaults here rereads them for this session without
|
|
37
|
+
replacing its explicit overrides. Other running sessions keep their current defaults.
|
|
38
|
+
|
|
39
|
+
`/skills enable NAME` and `/skills disable NAME` set one skill's session choice, like the picker;
|
|
40
|
+
other skills keep inheriting. `/skills all` and `/skills none` turn every listed skill on or off
|
|
41
|
+
for this session, and `/skills inherit` clears this session's skill choices.
|
|
42
|
+
|
|
43
|
+
Leaving and resuming a session rereads saved defaults. Explicit session choices stay; inherited
|
|
44
|
+
choices follow the current defaults. Resuming never reloads executable extensions or stops
|
|
45
|
+
children. New package registrations and native skill paths still need Pi's idle `/reload` or a
|
|
46
|
+
new invocation.
|
|
47
|
+
|
|
48
|
+
If a saved choice or session type is not valid (for example an old list instead of an ID map), that
|
|
49
|
+
category uses Pi's defaults and Pi shows a warning; `session_setup` also returns it. Other
|
|
50
|
+
categories and the active tools are unaffected. In an RPC or print session, `session_setup apply`
|
|
51
|
+
names any requested interactive-only resources it leaves out.
|
|
52
|
+
|
|
53
|
+
Older session records contain complete selection snapshots rather than per-item overrides. Those
|
|
54
|
+
snapshots stay explicit so an upgrade cannot guess which choices you meant to keep. Choose Inherit
|
|
55
|
+
for a row, or reset that category with `session_setup`, to follow saved defaults again.
|
|
56
|
+
|
|
57
|
+
A grey Global control means the resource only exists in this project or this `-e` run. The reason
|
|
58
|
+
shows in Overview and when you use the control. To make a package available in every project,
|
|
59
|
+
register its source in personal settings with Pi and include the resource in any package filters.
|
|
60
|
+
Saving defaults does not register sources. Package availability is saved separately in
|
|
61
|
+
`resourcePackages`; it never rewrites individual resource defaults or the native source list.
|
|
62
|
+
|
|
63
|
+
New delegated children follow saved package availability as headless sessions, so packages that
|
|
64
|
+
are off or Interactive only stay out. Package skills with no saved choice are off in children too.
|
|
65
|
+
An agent's explicit `skills` list cannot pull a skill from a hidden package; the launch warns.
|
|
66
|
+
|
|
67
|
+
## Picking is not installing
|
|
68
|
+
|
|
69
|
+
`pi install SOURCE` saves a personal package source, `pi install --local SOURCE` a project one,
|
|
70
|
+
and `pi -e SOURCE` tries a package for one run. Those are Pi commands. This extension never
|
|
71
|
+
installs packages, unloads code, changes permissions or restarts children. See
|
|
72
|
+
[package design](https://github.com/finngi/pi-packages/blob/main/docs/package-design.md).
|
|
73
|
+
|
|
74
|
+
`pi-delegation` owns `/agents` and child sessions; `pi-context` owns `/instructions` and adds
|
|
75
|
+
snippets to the prompt. All three share one `session_setup` service and session record format.
|
|
76
|
+
Without `pi-resources`, Pi loads skills as usual and delegation's skill rules still work. Children
|
|
77
|
+
get their launcher's explicit skills and never run a picker.
|
|
78
|
+
|
|
79
|
+
When you resume a session with recorded skill choices, load `pi-resources` again. Resources from
|
|
80
|
+
missing packages do not appear; their recorded overrides remain available if the resources return.
|
|
81
|
+
|
|
82
|
+
## Library APIs
|
|
83
|
+
|
|
84
|
+
Other extensions can import these without turning on `/skills`, `/packages` or parent skill
|
|
85
|
+
filtering:
|
|
86
|
+
|
|
87
|
+
| Import | Provides |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `pi-resources/discovery` | Resolved package resources and agent and instruction descriptors |
|
|
90
|
+
| `pi-resources/selection` | Saved personal and project selections and saving |
|
|
91
|
+
| `pi-resources/session-setup` | The shared session selection service, installed by calling a function |
|
|
92
|
+
| `pi-resources/instructions` | Requests saved instruction text from the loaded context extension |
|
|
93
|
+
| `pi-resources/picker` | Shared scope controls for skills, agents, instructions and package groups |
|
|
94
|
+
|
|
95
|
+
The package's default entrypoint is the extension, and the library subpaths never import it.
|
|
96
|
+
Delegation and context use these APIs but still load on their own; each installs the session setup
|
|
97
|
+
service it needs. Pi still owns installation, trust and filters. No import scans a new catalogue,
|
|
98
|
+
loads another extension or writes settings.
|
|
99
|
+
|
|
100
|
+
## Tests
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
npm run test --workspace=pi-resources
|
|
104
|
+
npm run typecheck --workspace=pi-resources
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Stock-Pi tests cover each extension on its own, both loading orders and resumed choices.
|
|
108
|
+
Entrypoint: [resources.ts](https://github.com/finngi/pi-packages/blob/main/packages/pi-resources/extensions/resources.ts).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 finngi2 contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# pi-resources
|
|
2
|
+
|
|
3
|
+
Choose which skills and packages a Pi session can use, without loading delegation, background
|
|
4
|
+
jobs or permissions. It adds `/skills`, `/packages` and the shared `session_setup` tool, using
|
|
5
|
+
Pi's loaded resources and the shared resource browser.
|
|
6
|
+
|
|
7
|
+
- `/skills` chooses which skill descriptions the parent's prompt lists. Skill bodies still load on
|
|
8
|
+
demand, and `/skill:name` is refused for skills that are off.
|
|
9
|
+
- `/packages` makes package resources available or hides them. It does not select their skills,
|
|
10
|
+
agents or instructions. Those choices stay independent, and return when a disabled package is
|
|
11
|
+
enabled again. Packages with only extensions are not listed: availability never changes which
|
|
12
|
+
extension code loads. Agents need `pi-delegation` and instructions need `pi-context`. Opening
|
|
13
|
+
the picker loads neither.
|
|
14
|
+
|
|
15
|
+
## Saving choices
|
|
16
|
+
|
|
17
|
+
Session, Project and Global each show their own Inherit, On or Off choice. The most specific
|
|
18
|
+
explicit choice wins; Inherit follows the broader scope, ending at Pi's default. Turning a skill
|
|
19
|
+
off for this session leaves its project and global choices intact. The picker shows the effective
|
|
20
|
+
choice and where it came from separately.
|
|
21
|
+
|
|
22
|
+
Project and Global also offer a session type: **Any session**, **Interactive only** or
|
|
23
|
+
**Type: Inherit**. Interactive means Pi's TUI; RPC (even with dialogs), print mode and delegated
|
|
24
|
+
children are excluded. Session type does not turn a resource on or off. A project type overrides
|
|
25
|
+
the global one. Session has no type of its own.
|
|
26
|
+
|
|
27
|
+
Ctrl+S copies eligible current On/Off selections to project defaults; Ctrl+Shift+S copies them to
|
|
28
|
+
personal defaults. Bulk saves leave session types alone. Changing a single scope control only
|
|
29
|
+
writes that resource at that scope. Editing defaults here rereads them for this session without
|
|
30
|
+
replacing its explicit overrides. Other running sessions keep their current defaults.
|
|
31
|
+
|
|
32
|
+
`/skills enable NAME` and `/skills disable NAME` set one skill's session choice, like the picker;
|
|
33
|
+
other skills keep inheriting. `/skills all` and `/skills none` turn every listed skill on or off
|
|
34
|
+
for this session, and `/skills inherit` clears this session's skill choices.
|
|
35
|
+
|
|
36
|
+
Leaving and resuming a session rereads saved defaults. Explicit session choices stay; inherited
|
|
37
|
+
choices follow the current defaults. Resuming never reloads executable extensions or stops
|
|
38
|
+
children. New package registrations and native skill paths still need Pi's idle `/reload` or a
|
|
39
|
+
new invocation.
|
|
40
|
+
|
|
41
|
+
If a saved choice or session type is not valid (for example an old list instead of an ID map), that
|
|
42
|
+
category uses Pi's defaults and Pi shows a warning; `session_setup` also returns it. Other
|
|
43
|
+
categories and the active tools are unaffected. In an RPC or print session, `session_setup apply`
|
|
44
|
+
names any requested interactive-only resources it leaves out.
|
|
45
|
+
|
|
46
|
+
Older session records contain complete selection snapshots rather than per-item overrides. Those
|
|
47
|
+
snapshots stay explicit so an upgrade cannot guess which choices you meant to keep. Choose Inherit
|
|
48
|
+
for a row, or reset that category with `session_setup`, to follow saved defaults again.
|
|
49
|
+
|
|
50
|
+
A grey Global control means the resource only exists in this project or this `-e` run. The reason
|
|
51
|
+
shows in Overview and when you use the control. To make a package available in every project,
|
|
52
|
+
register its source in personal settings with Pi and include the resource in any package filters.
|
|
53
|
+
Saving defaults does not register sources. Package availability is saved separately in
|
|
54
|
+
`resourcePackages`; it never rewrites individual resource defaults or the native source list.
|
|
55
|
+
|
|
56
|
+
New delegated children follow saved package availability as headless sessions, so packages that
|
|
57
|
+
are off or Interactive only stay out. Package skills with no saved choice are off in children too.
|
|
58
|
+
An agent's explicit `skills` list cannot pull a skill from a hidden package; the launch warns.
|
|
59
|
+
|
|
60
|
+
## Picking is not installing
|
|
61
|
+
|
|
62
|
+
`pi install SOURCE` saves a personal package source, `pi install --local SOURCE` a project one,
|
|
63
|
+
and `pi -e SOURCE` tries a package for one run. Those are Pi commands. This extension never
|
|
64
|
+
installs packages, unloads code, changes permissions or restarts children. See
|
|
65
|
+
[package design](../../docs/package-design.md).
|
|
66
|
+
|
|
67
|
+
`pi-delegation` owns `/agents` and child sessions; `pi-context` owns `/instructions` and adds
|
|
68
|
+
snippets to the prompt. All three share one `session_setup` service and session record format.
|
|
69
|
+
Without `pi-resources`, Pi loads skills as usual and delegation's skill rules still work. Children
|
|
70
|
+
get their launcher's explicit skills and never run a picker.
|
|
71
|
+
|
|
72
|
+
When you resume a session with recorded skill choices, load `pi-resources` again. Resources from
|
|
73
|
+
missing packages do not appear; their recorded overrides remain available if the resources return.
|
|
74
|
+
|
|
75
|
+
## Library APIs
|
|
76
|
+
|
|
77
|
+
Other extensions can import these without turning on `/skills`, `/packages` or parent skill
|
|
78
|
+
filtering:
|
|
79
|
+
|
|
80
|
+
| Import | Provides |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| `pi-resources/discovery` | Resolved package resources and agent and instruction descriptors |
|
|
83
|
+
| `pi-resources/selection` | Saved personal and project selections and saving |
|
|
84
|
+
| `pi-resources/session-setup` | The shared session selection service, installed by calling a function |
|
|
85
|
+
| `pi-resources/instructions` | Requests saved instruction text from the loaded context extension |
|
|
86
|
+
| `pi-resources/picker` | Shared scope controls for skills, agents, instructions and package groups |
|
|
87
|
+
|
|
88
|
+
The package's default entrypoint is the extension, and the library subpaths never import it.
|
|
89
|
+
Delegation and context use these APIs but still load on their own; each installs the session setup
|
|
90
|
+
service it needs. Pi still owns installation, trust and filters. No import scans a new catalogue,
|
|
91
|
+
loads another extension or writes settings.
|
|
92
|
+
|
|
93
|
+
## Tests
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
npm run test --workspace=pi-resources
|
|
97
|
+
npm run typecheck --workspace=pi-resources
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Stock-Pi tests cover each extension on its own, both loading orders and resumed choices.
|
|
101
|
+
Entrypoint: [resources.ts](extensions/resources.ts).
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { getAgentDir, loadSkills, parseFrontmatter, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { SESSION_SETUP_CONTRIBUTIONS } from "pi-contracts";
|
|
6
|
+
import { showResourceBrowser, resourceBrowserNavigation, } from "pi-resource-browser";
|
|
7
|
+
import { configuredSkillChoices, filterPromptSkills, selectedSkills, } from "pi-resources/selection";
|
|
8
|
+
import { discoverResourcePackages, resourcePackageKeywords, } from "pi-resources/discovery";
|
|
9
|
+
import { installSessionSetup } from "pi-resources/session-setup";
|
|
10
|
+
import { resourcePickerControls } from "pi-resources/picker";
|
|
11
|
+
import { registerPackagePicker } from "../lib/package-picker.js";
|
|
12
|
+
/** Session-only selection of native loaded resources. Importing shared helpers has no effect. */
|
|
13
|
+
export default function (pi) {
|
|
14
|
+
// Children already receive an explicit public/private skill set from their launcher.
|
|
15
|
+
if (process.env.PI_SUBAGENT === "1")
|
|
16
|
+
return;
|
|
17
|
+
const setup = installSessionSetup(pi);
|
|
18
|
+
let packageResources = {
|
|
19
|
+
packs: [],
|
|
20
|
+
problems: [],
|
|
21
|
+
};
|
|
22
|
+
let nativePersonalSkillPaths = new Set();
|
|
23
|
+
let skillChoicesSnapshot = [];
|
|
24
|
+
let skillIdentities = new Map();
|
|
25
|
+
const errorText = (error) => (error instanceof Error ? error.message : String(error));
|
|
26
|
+
const skills = () => pi
|
|
27
|
+
.getCommands()
|
|
28
|
+
.filter((c) => c.source === "skill")
|
|
29
|
+
.map((c) => ({
|
|
30
|
+
name: c.name.replace(/^skill:/, ""),
|
|
31
|
+
description: c.description,
|
|
32
|
+
path: c.sourceInfo.path,
|
|
33
|
+
source: c.sourceInfo,
|
|
34
|
+
}));
|
|
35
|
+
const personalSkillDefault = (skill) => {
|
|
36
|
+
const pack = packageResources.packs.find((pack) => pack.name === skill.packName);
|
|
37
|
+
// Native metadata may label a CLI-enabled skill as user-scoped even when the personal
|
|
38
|
+
// package excludes it. Only the resolver's enabled personal members are defaultable.
|
|
39
|
+
if (pack)
|
|
40
|
+
return pack.globalDefaultable !== false && !!pack.personalSkills?.includes(skill.path);
|
|
41
|
+
return !skill.defaultSelected || nativePersonalSkillPaths.has(skill.path);
|
|
42
|
+
};
|
|
43
|
+
const settingsManager = (ctx) => SettingsManager.create(ctx.cwd, getAgentDir(), { projectTrusted: ctx.isProjectTrusted() });
|
|
44
|
+
const selectedPublicSkills = (ctx) => {
|
|
45
|
+
const manager = settingsManager(ctx);
|
|
46
|
+
const choices = currentSkillChoices(ctx);
|
|
47
|
+
const selected = setup.selection("skills");
|
|
48
|
+
if (selected !== undefined) {
|
|
49
|
+
const missing = selected.filter((name) => !choices.some((choice) => choice.name === name));
|
|
50
|
+
if (missing.length)
|
|
51
|
+
throw Error(`Selected skills are no longer registered or unchanged: ${missing.join(", ")}. Reload resources.`);
|
|
52
|
+
return choices.filter((choice) => selected.includes(choice.name));
|
|
53
|
+
}
|
|
54
|
+
return selectedSkills(manager, choices, false, undefined, ctx.mode === "tui");
|
|
55
|
+
};
|
|
56
|
+
const currentSkillChoices = (ctx) => {
|
|
57
|
+
const registered = skills();
|
|
58
|
+
return skillChoicesSnapshot.filter((choice) => {
|
|
59
|
+
try {
|
|
60
|
+
if (!registered.some((skill) => skill.name === choice.name &&
|
|
61
|
+
resolve(ctx.cwd, skill.path) === resolve(ctx.cwd, choice.path)))
|
|
62
|
+
return false;
|
|
63
|
+
const info = statSync(choice.path), identity = skillIdentities.get(choice.path);
|
|
64
|
+
if (!identity || !info.isFile() || info.dev !== identity.dev || info.ino !== identity.ino)
|
|
65
|
+
return false;
|
|
66
|
+
const actual = loadSkills({
|
|
67
|
+
cwd: ctx.cwd,
|
|
68
|
+
agentDir: getAgentDir(),
|
|
69
|
+
skillPaths: [choice.path],
|
|
70
|
+
includeDefaults: false,
|
|
71
|
+
}).skills;
|
|
72
|
+
return actual.some((skill) => skill.name === choice.name && skill.filePath === choice.path);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
const refreshResources = async (ctx) => {
|
|
80
|
+
packageResources = await discoverResourcePackages(settingsManager(ctx), ctx.cwd, getAgentDir(), skills().map((skill) => skill.source));
|
|
81
|
+
// Only resources already loaded by native Pi can enter this session selection.
|
|
82
|
+
nativePersonalSkillPaths = new Set(skills()
|
|
83
|
+
.filter((skill) => skill.source.scope === "user")
|
|
84
|
+
.map((skill) => skill.path));
|
|
85
|
+
skillChoicesSnapshot = configuredSkillChoices(ctx.cwd, getAgentDir(), skills().map((skill) => ({ name: skill.name, path: skill.path })), packageResources.packs);
|
|
86
|
+
skillIdentities = new Map();
|
|
87
|
+
skillChoicesSnapshot = skillChoicesSnapshot.filter((skill) => {
|
|
88
|
+
try {
|
|
89
|
+
const { dev, ino } = statSync(skill.path);
|
|
90
|
+
skillIdentities.set(skill.path, { dev, ino });
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
pi.on("session_start", async (_input, ctx) => refreshResources(ctx));
|
|
99
|
+
pi.on("input", (event, ctx) => {
|
|
100
|
+
const match = /^\/skill:([^\s]+)(?:\s|$)/.exec(event.text);
|
|
101
|
+
if (!match)
|
|
102
|
+
return;
|
|
103
|
+
try {
|
|
104
|
+
const selected = new Set(selectedPublicSkills(ctx).map((skill) => skill.name));
|
|
105
|
+
if (selected.has(match[1]))
|
|
106
|
+
return;
|
|
107
|
+
ctx.ui.notify(`Skill "${match[1]}" is not selected for this session. Use /skills to choose it.`, "warning");
|
|
108
|
+
return { action: "handled" };
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
ctx.ui.notify(`Could not validate skill selection: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
112
|
+
return { action: "handled" };
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
pi.on("before_agent_start", (input, ctx) => {
|
|
116
|
+
try {
|
|
117
|
+
input.systemPromptOptions.skills = filterPromptSkills(input.systemPromptOptions.skills ?? [], selectedPublicSkills(ctx));
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
input.systemPromptOptions.skills = [];
|
|
121
|
+
if (ctx.hasUI)
|
|
122
|
+
ctx.ui.notify(`Could not validate selected skills; no skills were added to the prompt. Run /skills to inspect settings. (${errorText(error)})`, "warning");
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
pi.events.on(SESSION_SETUP_CONTRIBUTIONS, (value) => {
|
|
126
|
+
const request = value;
|
|
127
|
+
if (request?.version !== 1 || typeof request.contribute !== "function")
|
|
128
|
+
return;
|
|
129
|
+
const ctx = request.ctx, manager = settingsManager(ctx);
|
|
130
|
+
const choices = currentSkillChoices(ctx);
|
|
131
|
+
let selectedSkillIds = [];
|
|
132
|
+
try {
|
|
133
|
+
selectedSkillIds = selectedSkills(manager, choices, false, undefined, ctx.mode === "tui").map((skill) => skill.name);
|
|
134
|
+
}
|
|
135
|
+
catch { }
|
|
136
|
+
request.contribute({
|
|
137
|
+
kind: "skills",
|
|
138
|
+
candidates: choices.map((skill) => ({
|
|
139
|
+
kind: "skills",
|
|
140
|
+
id: skill.name,
|
|
141
|
+
name: skill.name,
|
|
142
|
+
description: skill.description,
|
|
143
|
+
path: skill.path,
|
|
144
|
+
nativeDefault: skill.defaultSelected,
|
|
145
|
+
globalDefaultable: personalSkillDefault(skill),
|
|
146
|
+
...(skill.manualOnly ? { manualOnly: true } : {}),
|
|
147
|
+
...(skill.packName ? { packName: skill.packName, keywords: skill.packKeywords } : {}),
|
|
148
|
+
})),
|
|
149
|
+
selected: selectedSkillIds,
|
|
150
|
+
});
|
|
151
|
+
request.contribute({
|
|
152
|
+
kind: "packages",
|
|
153
|
+
candidates: packageResources.packs.map((pack) => ({
|
|
154
|
+
kind: "packages",
|
|
155
|
+
id: pack.name,
|
|
156
|
+
name: pack.name,
|
|
157
|
+
description: pack.description,
|
|
158
|
+
keywords: resourcePackageKeywords(pack),
|
|
159
|
+
globalDefaultable: pack.globalDefaultable,
|
|
160
|
+
nativeDefault: true,
|
|
161
|
+
})),
|
|
162
|
+
selected: [],
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
registerPackagePicker(pi, setup, () => packageResources.problems);
|
|
166
|
+
const selectionArgs = (args) => {
|
|
167
|
+
const match = /^(enable|disable|none|all|inherit|inspect)(?:\s+(.+))?$/.exec(args.trim());
|
|
168
|
+
return match ? { action: match[1], name: match[2]?.trim() } : undefined;
|
|
169
|
+
};
|
|
170
|
+
pi.registerCommand("skills", {
|
|
171
|
+
description: "Select skills for this session; Ctrl+S saves project defaults; Ctrl+Shift+S saves personal defaults",
|
|
172
|
+
async handler(args, ctx) {
|
|
173
|
+
const command = args.trim();
|
|
174
|
+
const action = selectionArgs(command);
|
|
175
|
+
const repairingInvalidProjectSelection = action?.action === "none" || action?.action === "all" || action?.action === "inherit";
|
|
176
|
+
const manager = settingsManager(ctx);
|
|
177
|
+
const loaded = skills().map((skill) => ({ name: skill.name, path: skill.path }));
|
|
178
|
+
const available = new Set(setup
|
|
179
|
+
.inspect(ctx)
|
|
180
|
+
.find((value) => value.kind === "skills")
|
|
181
|
+
?.candidates.map((candidate) => candidate.id));
|
|
182
|
+
const choices = configuredSkillChoices(ctx.cwd, getAgentDir(), loaded, packageResources.packs).filter((choice) => available.has(choice.name));
|
|
183
|
+
const selected = selectedSkills(manager, choices, repairingInvalidProjectSelection, undefined, ctx.mode === "tui");
|
|
184
|
+
const selectedNames = new Set(setup.selection("skills") ?? selected.map((skill) => skill.name));
|
|
185
|
+
if (action) {
|
|
186
|
+
// Session choices are per skill, like the picker; unlisted skills keep inheriting.
|
|
187
|
+
const toggleable = choices.filter((skill) => skill.toggleable);
|
|
188
|
+
if (action.action === "none" || action.action === "all") {
|
|
189
|
+
const state = action.action === "all" ? "on" : "off";
|
|
190
|
+
setup.setStates("skills", Object.fromEntries(toggleable.map((skill) => [skill.name, state])), ctx);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
else if (action.action === "inherit") {
|
|
194
|
+
setup.execute({ op: "reset", kinds: ["skills"] }, ctx);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
else if (action.action === "enable" || action.action === "disable") {
|
|
198
|
+
const choice = toggleable.find((skill) => skill.name === action.name);
|
|
199
|
+
if (!choice)
|
|
200
|
+
throw Error(`Unknown selectable public skill "${action.name ?? ""}"`);
|
|
201
|
+
setup.setState("skills", choice.name, action.action === "enable" ? "on" : "off", ctx);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
else if (action.action === "inspect" && !action.name) {
|
|
205
|
+
throw Error("Use /skills inspect NAME");
|
|
206
|
+
}
|
|
207
|
+
else if (action.action !== "inspect") {
|
|
208
|
+
throw Error(`Unsupported skill selection command: ${action.action}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
let inspectName = action?.action === "inspect" ? action.name : undefined;
|
|
212
|
+
if (!action && command)
|
|
213
|
+
inspectName = command;
|
|
214
|
+
if (!inspectName && !action && ctx.hasUI && choices.length) {
|
|
215
|
+
const entries = choices.map((skill) => {
|
|
216
|
+
return {
|
|
217
|
+
id: skill.name,
|
|
218
|
+
name: skill.name,
|
|
219
|
+
description: skill.description,
|
|
220
|
+
keywords: `${skill.manualOnly ? "manual command" : "model skill"} ${skill.packKeywords ?? ""}`,
|
|
221
|
+
selected: selectedNames.has(skill.name),
|
|
222
|
+
globalDefaultable: personalSkillDefault(skill),
|
|
223
|
+
toggleable: skill.toggleable,
|
|
224
|
+
badge: skill.packName ?? (skill.manualOnly ? "manual" : undefined),
|
|
225
|
+
preview: async () => {
|
|
226
|
+
const { body, frontmatter } = parseFrontmatter(await readFile(skill.path, "utf8"));
|
|
227
|
+
const invocation = frontmatter["disable-model-invocation"] === true
|
|
228
|
+
? "Manual command only"
|
|
229
|
+
: "Description advertised when selected";
|
|
230
|
+
return {
|
|
231
|
+
fields: [{ label: "Invocation", value: invocation }],
|
|
232
|
+
source: skill.path,
|
|
233
|
+
body,
|
|
234
|
+
};
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
await showResourceBrowser(ctx, {
|
|
239
|
+
title: "Skills",
|
|
240
|
+
...resourceBrowserNavigation(pi, "skills"),
|
|
241
|
+
selectionScope: "session",
|
|
242
|
+
defaultsScope: "project",
|
|
243
|
+
entries,
|
|
244
|
+
...resourcePickerControls({
|
|
245
|
+
ctx,
|
|
246
|
+
setup,
|
|
247
|
+
entries,
|
|
248
|
+
members: (entry) => [
|
|
249
|
+
{
|
|
250
|
+
kind: "skills",
|
|
251
|
+
id: entry.id,
|
|
252
|
+
nativeDefault: choices.find((choice) => choice.name === entry.id)?.defaultSelected,
|
|
253
|
+
globalDefaultable: entry.globalDefaultable,
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
}),
|
|
257
|
+
});
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (inspectName) {
|
|
261
|
+
const skill = choices.find((item) => item.name === inspectName);
|
|
262
|
+
if (!skill)
|
|
263
|
+
throw Error(`Unknown public skill "${inspectName}"`);
|
|
264
|
+
ctx.ui.notify(`${skill.name} — ${skill.description}\n${skill.path}`, "info");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const rows = [
|
|
268
|
+
...choices.map((skill) => `${selectedNames.has(skill.name) ? "[x]" : "[ ]"} ${skill.name} — ${skill.description}\n${skill.path}`),
|
|
269
|
+
];
|
|
270
|
+
ctx.ui.notify(rows.join("\n\n") || "No public skills are available.", "info");
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { installSessionSetup } from "pi-resources/session-setup";
|
|
3
|
+
export declare function registerPackagePicker(pi: ExtensionAPI, setup: ReturnType<typeof installSessionSetup>, diagnostics: () => string[]): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { showResourceBrowser, resourceBrowserNavigation, } from "pi-resource-browser";
|
|
2
|
+
import { resourcePickerControls } from "./picker.js";
|
|
3
|
+
export function registerPackagePicker(pi, setup, diagnostics) {
|
|
4
|
+
pi.registerCommand("packages", {
|
|
5
|
+
description: "Choose available packages without selecting their contents",
|
|
6
|
+
async handler(_args, ctx) {
|
|
7
|
+
await ctx.waitForIdle();
|
|
8
|
+
const inspect = () => setup.inspect(ctx);
|
|
9
|
+
const contributions = inspect();
|
|
10
|
+
const entries = contributions
|
|
11
|
+
.filter((value) => value.kind === "packages")
|
|
12
|
+
.flatMap((value) => value.candidates)
|
|
13
|
+
.map((pack) => ({
|
|
14
|
+
id: pack.id,
|
|
15
|
+
name: pack.name.replace(/^@[^/]+\/pi-/, ""),
|
|
16
|
+
description: pack.description,
|
|
17
|
+
keywords: pack.keywords,
|
|
18
|
+
selected: false,
|
|
19
|
+
globalDefaultable: pack.globalDefaultable,
|
|
20
|
+
preview: async () => ({
|
|
21
|
+
fields: [
|
|
22
|
+
{ label: "Package", value: pack.id },
|
|
23
|
+
{
|
|
24
|
+
label: "Resources",
|
|
25
|
+
value: "Discoverable when enabled; select contents separately.",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
label: "Code",
|
|
29
|
+
value: "Native extensions load at startup; code changes need restart.",
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
}),
|
|
33
|
+
}));
|
|
34
|
+
if (!ctx.hasUI) {
|
|
35
|
+
ctx.ui.notify("Use session_setup to select package guidance in a headless session.", "info");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
await showResourceBrowser(ctx, {
|
|
39
|
+
title: "Packages",
|
|
40
|
+
...resourceBrowserNavigation(pi, "packages"),
|
|
41
|
+
entries,
|
|
42
|
+
selectionScope: "session",
|
|
43
|
+
defaultsScope: "project",
|
|
44
|
+
notice: diagnostics().join("\n") || undefined,
|
|
45
|
+
...resourcePickerControls({
|
|
46
|
+
ctx,
|
|
47
|
+
setup,
|
|
48
|
+
entries,
|
|
49
|
+
members: (entry) => [
|
|
50
|
+
{
|
|
51
|
+
kind: "packages",
|
|
52
|
+
id: entry.id,
|
|
53
|
+
nativeDefault: true,
|
|
54
|
+
globalDefaultable: entry.globalDefaultable,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ResourceEntry, ResourceDefaultState, ResourceSessionMode } from "pi-resource-browser";
|
|
3
|
+
import { type ResourceKind } from "./resource-selection.ts";
|
|
4
|
+
import type { installSessionSetup } from "./session-setup.ts";
|
|
5
|
+
export interface PickerMember {
|
|
6
|
+
kind: ResourceKind;
|
|
7
|
+
id: string;
|
|
8
|
+
nativeDefault?: boolean;
|
|
9
|
+
globalDefaultable?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** One scope implementation for independent resources and package availability. */
|
|
12
|
+
export declare function resourcePickerControls(options: {
|
|
13
|
+
ctx: ExtensionCommandContext;
|
|
14
|
+
setup: ReturnType<typeof installSessionSetup>;
|
|
15
|
+
entries: ResourceEntry[];
|
|
16
|
+
members(entry: ResourceEntry): PickerMember[];
|
|
17
|
+
}): {
|
|
18
|
+
refresh: () => void;
|
|
19
|
+
toggle: (entry: ResourceEntry, selected: boolean) => Promise<void>;
|
|
20
|
+
setSession: (entry: ResourceEntry, state: ResourceDefaultState) => Promise<void>;
|
|
21
|
+
setDefault: (entry: ResourceEntry, state: ResourceDefaultState, scope: "project" | "global") => Promise<void>;
|
|
22
|
+
setMode: (entry: ResourceEntry, mode: ResourceSessionMode, scope: "project" | "global") => Promise<void>;
|
|
23
|
+
saveDefaults: (rows: ResourceEntry[]) => Promise<void>;
|
|
24
|
+
saveGlobalDefaults: (rows: ResourceEntry[]) => Promise<void>;
|
|
25
|
+
};
|