pi-session-mcp 0.7.0 → 0.8.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/.pi-session-mcp-cli/config.js +47 -7
- package/.pi-session-mcp-cli/doctor.js +68 -2
- package/.pi-session-mcp-cli/execution-profile.js +1 -0
- package/.pi-session-mcp-cli/extension-spec.js +7 -2
- package/.pi-session-mcp-cli/setup-result.js +8 -0
- package/.pi-session-mcp-cli/skill-catalog.js +35 -0
- package/.pi-session-mcp-cli/skill-customize.js +183 -0
- package/.pi-session-mcp-cli/skill-library.js +120 -0
- package/CHANGELOG.md +110 -0
- package/README.md +15 -3
- package/dist/capabilities.d.ts +1 -0
- package/dist/capabilities.js +2 -1
- package/dist/client-adapters/mcp-verifier.js +6 -2
- package/dist/config.d.ts +4 -0
- package/dist/config.js +47 -7
- package/dist/doctor.d.ts +4 -1
- package/dist/doctor.js +68 -2
- package/dist/execution-profile.d.ts +4 -0
- package/dist/execution-profile.js +1 -0
- package/dist/extension-spec.js +7 -2
- package/dist/pi-adapter.d.ts +1 -1
- package/dist/pi-adapter.js +1 -0
- package/dist/pi-skills.d.ts +17 -0
- package/dist/pi-skills.js +64 -0
- package/dist/sdk-pi-adapter.d.ts +7 -3
- package/dist/sdk-pi-adapter.js +57 -11
- package/dist/server.js +1 -0
- package/dist/session-tools.d.ts +1 -1
- package/dist/session-tools.js +20 -4
- package/dist/setup-cli.d.ts +2 -0
- package/dist/setup-cli.js +16 -0
- package/dist/setup-main.d.ts +1 -1
- package/dist/setup-main.js +18 -3
- package/dist/setup-ownership.d.ts +6 -0
- package/dist/setup-ownership.js +29 -5
- package/dist/setup-skills.d.ts +37 -0
- package/dist/setup-skills.js +353 -0
- package/dist/setup.d.ts +7 -0
- package/dist/setup.js +49 -6
- package/dist/skill-catalog.d.ts +26 -0
- package/dist/skill-catalog.js +35 -0
- package/dist/skill-customize.d.ts +23 -0
- package/dist/skill-customize.js +183 -0
- package/dist/skill-library.d.ts +61 -0
- package/dist/skill-library.js +120 -0
- package/docs/acceptance.md +372 -3
- package/docs/adr/0006-pi-extension-allowlist.md +5 -2
- package/docs/adr/0007-profile-skill-selection.md +217 -0
- package/docs/architecture.md +9 -5
- package/docs/client-setup-design.md +133 -1
- package/docs/client-setup.md +74 -3
- package/docs/doctor-design.md +9 -1
- package/docs/installation.md +129 -9
- package/docs/pi-extensions-design.md +41 -12
- package/docs/releasing.md +15 -6
- package/docs/session-mcp.md +7 -2
- package/docs/skills.md +487 -0
- package/docs/threat-model.md +21 -4
- package/docs/tool-contracts.md +9 -5
- package/package.json +3 -1
- package/pi-session-mcp.example.json +9 -3
- package/skills/pi-session-implementation/SKILL.md +42 -0
- package/skills/pi-session-orchestration/SKILL.md +93 -0
- package/skills/pi-session-review/SKILL.md +58 -0
- package/docs/superpowers/plans/2026-10-10-macos-setup.md +0 -1725
- package/docs/superpowers/plans/2026-10-11-pi-extensions.md +0 -2450
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,116 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.8.0 — 2026-10-12
|
|
6
|
+
|
|
7
|
+
This is a minor release because the public contract changes, as ADR 0003
|
|
8
|
+
requires: the `skills` field of each execution profile in
|
|
9
|
+
`pi_capabilities_get` and the new error code `skill_load_failed`.
|
|
10
|
+
|
|
11
|
+
- Execution profiles can select Pi skills. An optional top-level
|
|
12
|
+
`skillCatalog` maps a skill alias to the path of one `SKILL.md` file (or to
|
|
13
|
+
`builtin:<name>`, see below), and an
|
|
14
|
+
optional `skills` list in a profile selects catalog aliases. MCP callers still
|
|
15
|
+
choose only workspace and profile aliases; no tool takes a skill. A session
|
|
16
|
+
loads exactly the selected skills and validates them at start; a missing,
|
|
17
|
+
unreadable, oversized or invalid skill, a duplicate Pi skill name, two aliases
|
|
18
|
+
on one file, or an explicit-only skill (`disable-model-invocation: true`)
|
|
19
|
+
fails the start with the new error `skill_load_failed`. Every execution
|
|
20
|
+
profile in `pi_capabilities_get` gains `skills`, the selected catalog aliases
|
|
21
|
+
(`[]` when none). Doctor gains `skill_missing`, `skill_unreadable` and
|
|
22
|
+
`skill_duplicate`. Skill file contents are not snapshotted; do not change
|
|
23
|
+
them while sessions that use them are active. See [Pi
|
|
24
|
+
skills](docs/skills.md) and [ADR 0007](docs/adr/0007-profile-skill-selection.md).
|
|
25
|
+
Issue #46.
|
|
26
|
+
- Compatibility change: with Pi SDK 1.1.0, skills that an allowlisted
|
|
27
|
+
extension package bundles reached a session although `noSkills` was set. An
|
|
28
|
+
extension allowlist alone now makes none of its skills available. A profile
|
|
29
|
+
that relied on them needs a catalog entry with the absolute path of the
|
|
30
|
+
package's `SKILL.md` and a `skills` selection; see the migration section of
|
|
31
|
+
[Pi skills](docs/skills.md).
|
|
32
|
+
- Prompt templates and themes that extension packages ship are now closed
|
|
33
|
+
through the SDK's public overrides as well. They were already never expanded
|
|
34
|
+
for MCP prompts.
|
|
35
|
+
- The package ships three skills under `skills/`: `pi-session-orchestration`
|
|
36
|
+
for the controlling Claude Code or Codex agent, and `pi-session-implementation`
|
|
37
|
+
and `pi-session-review` for Pi workers. The example configuration registers
|
|
38
|
+
the two worker skills with `builtin:` references, so a copied example needs
|
|
39
|
+
no path edits for skills.
|
|
40
|
+
- Skill library and `builtin:`. A `skillCatalog` value `builtin:<name>` names a
|
|
41
|
+
skill of the library: the operator's override
|
|
42
|
+
`<configuration directory>/skills/<name>/SKILL.md` if it exists, else the
|
|
43
|
+
skill shipped in the package. Resolution happens when the configuration
|
|
44
|
+
loads, so adding or removing an override needs a restart. A name that matches
|
|
45
|
+
neither keeps the shipped location, so Doctor reports `skill_missing` and a
|
|
46
|
+
session start fails with `skill_load_failed`. A malformed `builtin:` name makes
|
|
47
|
+
the configuration invalid.
|
|
48
|
+
- `pi-session-mcp-setup --customize-skill <name>` (a dry-run; `--apply` writes)
|
|
49
|
+
copies a shipped skill into the override directory as a starting point and
|
|
50
|
+
records the shipped fingerprint in `.pi-session-mcp-bases.json`. It never
|
|
51
|
+
overwrites an existing override. Findings use the target
|
|
52
|
+
`skills/override/<name>` and the new codes `skill_customize_planned`,
|
|
53
|
+
`skill_customized`, `skill_override_current`, `skill_override_outdated`,
|
|
54
|
+
`skill_base_updated`, `skill_not_shipped`, `skill_bases_corrupt`,
|
|
55
|
+
`skill_override_invalid` and `skill_customize_failed`. After a package
|
|
56
|
+
update changed a shipped skill, `--apply --customize-skill <name>` moves only
|
|
57
|
+
the recorded base once you reviewed the change; it never touches your files.
|
|
58
|
+
A new skill needs no command: create `<override dir>/<name>/SKILL.md` and
|
|
59
|
+
reference it as `builtin:<name>`.
|
|
60
|
+
- `pi-session-mcp-setup --apply` now also installs the controller skill
|
|
61
|
+
`pi-session-orchestration` for each target, after the registration, as a real
|
|
62
|
+
copy of the effective skill (override, else shipped): Codex `~/.agents/skills/`,
|
|
63
|
+
Claude Code `user` and `local` `~/.claude/skills/` (one shared directory),
|
|
64
|
+
Claude Code `project` `<project>/.claude/skills/`. The ownership record holds
|
|
65
|
+
only a content fingerprint. Setup never replaces or deletes a modified copy, a
|
|
66
|
+
foreign skill of the same name, hidden operator files inside the copy, or a
|
|
67
|
+
linked or non-directory entry (`skill_divergent`); a link to exactly the
|
|
68
|
+
effective skill is accepted as the operator's own (`skill_linked`). Inside a
|
|
69
|
+
project setup never follows a link, and a project copy is bound to its
|
|
70
|
+
project. A setup result has a second finding per target with the alias
|
|
71
|
+
`pi-session-orchestration` whenever the skill step runs (not for an apply
|
|
72
|
+
that the plan refuses or a registration that failed); the new codes are `skill_absent`,
|
|
73
|
+
`skill_equivalent`, `skill_linked`, `skill_update_planned`, `skill_source_invalid`,
|
|
74
|
+
`skill_installed`, `skill_already_equivalent`, `skill_updated`,
|
|
75
|
+
`skill_install_failed`, `skill_verified`, `skill_missing`,
|
|
76
|
+
`skill_not_equivalent`, `skill_removed`, `skill_kept_shared`,
|
|
77
|
+
`skill_not_owned` and `skill_divergent`. A divergent skill does not block the
|
|
78
|
+
registration but makes the overall status `failed`. `--remove` and
|
|
79
|
+
`--rollback` remove an unmodified copy, and a retry still cleans up a skill
|
|
80
|
+
that setup owns after the registration is gone. Manual symlinking of the
|
|
81
|
+
skill is no longer needed.
|
|
82
|
+
- Doctor gains the warning `skill_override_outdated` (subject `skill:<name>`,
|
|
83
|
+
or `skill:bases` for a corrupt bases file) when a shipped skill changed since
|
|
84
|
+
the operator copied it.
|
|
85
|
+
- Maintainers get `scripts/multi-session-acceptance.mjs`, a live check in which
|
|
86
|
+
a real Claude Code or Codex controller drives two concurrent Pi sessions
|
|
87
|
+
with selected skills, including a targeted abort. It is not part of the npm
|
|
88
|
+
package; see [acceptance](docs/acceptance.md#multi-session-skill-acceptance).
|
|
89
|
+
|
|
90
|
+
## 0.7.1 — 2026-10-11
|
|
91
|
+
|
|
92
|
+
The public MCP contract is unchanged.
|
|
93
|
+
|
|
94
|
+
- Extension tool names are now limited to 1 to 64 letters, digits, `_` and
|
|
95
|
+
`-`, not starting with `-`, so `.` and `:` no longer pass and every name fits
|
|
96
|
+
the common provider function-name limit. Names that Pi SDK 1.1.0 treats as
|
|
97
|
+
MCP tools (the `mcp__` prefix, `list_mcp_resources`,
|
|
98
|
+
`list_mcp_resource_templates`, `read_mcp_resource`) are rejected too, because
|
|
99
|
+
the SDK would keep them outside the allowlist. Both cases fail with
|
|
100
|
+
`extension_tool_conflict`. Fixes #41.
|
|
101
|
+
- Extension pins with build metadata, such as `npm:<name>@1.2.3+build.1`, are
|
|
102
|
+
now rejected when the configuration loads. npm does not keep build metadata
|
|
103
|
+
in a published version, so such a pin could never match the installed
|
|
104
|
+
manifest. Such a configuration is now invalid as a whole: the server does not
|
|
105
|
+
start and Doctor reports `config_invalid`, instead of every session of the
|
|
106
|
+
profile failing with `extension_version_mismatch`. Fixes #42.
|
|
107
|
+
- Client prompts now reach Pi with `source: "rpc"`, the marker Pi's own RPC
|
|
108
|
+
mode uses for input from a program, instead of the SDK default
|
|
109
|
+
`interactive`. Extension `input` hooks that read the source now see that a
|
|
110
|
+
program, not a human at the terminal, sent the prompt. Fixes #43.
|
|
111
|
+
- The npm package no longer ships the implementation plans under
|
|
112
|
+
`docs/superpowers/`; the operator documentation stays included. A test
|
|
113
|
+
checks the packed file list. Fixes #39.
|
|
114
|
+
|
|
5
115
|
## 0.7.0 — 2026-10-11
|
|
6
116
|
|
|
7
117
|
This is a minor release because the public contract changes, as ADR 0003
|
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ Reasons to do that:
|
|
|
18
18
|
- **Follow-up turns keep their context.** A session stays open across prompts, so a follow-up question does not pay for the same exploration twice.
|
|
19
19
|
- **You set the limits, not the agent.** The client can only pick from the workspaces and profiles in your configuration file. It cannot name a directory, a model, a provider or a permission level, and the default profile is read-only.
|
|
20
20
|
- **Exactly the extra tools a task needs.** A profile can grant individual tools of local stdio MCP servers to its sessions, and nothing else from those servers.
|
|
21
|
+
- **Role instructions you control.** A profile can select Pi skills from a catalog in your configuration, such as the implementation and review skills this package ships, or your own versions of them. The client cannot choose skills. The package also ships an orchestration skill for the controlling agent, which `setup` installs into Claude Code and Codex.
|
|
21
22
|
- **Predictable to automate.** Sessions and turns have a small set of states and stable error codes. Results contain no paths, credentials, prompts or raw provider errors.
|
|
22
23
|
- **Small footprint.** No daemon, no network listener and no stored state. An offline `doctor` checks an installation, and `setup` registers, verifies and removes the server reproducibly.
|
|
23
24
|
|
|
@@ -29,7 +30,7 @@ It is not the right tool when:
|
|
|
29
30
|
|
|
30
31
|
## Status
|
|
31
32
|
|
|
32
|
-
The package version is **0.
|
|
33
|
+
The package version is **0.8.0**.
|
|
33
34
|
|
|
34
35
|
- **Pre-1.0.** The public tool contract is stable within a minor line: a change to tools, fields, states or error codes needs a new minor release.
|
|
35
36
|
- **Linux and macOS**, Node.js 22.19.0 or newer. macOS is tested in CI on macOS 15 and 26 on Apple silicon; Intel Macs run the same code but are untested. Setup acceptance on a real Mac is recorded for Claude Code only; Codex is still pending. See [Acceptance](docs/acceptance.md#status). On other platforms `setup` refuses with `platform_unsupported`.
|
|
@@ -76,9 +77,17 @@ pi-session-mcp-setup --apply --target claude-code:user:pi-session-mcp
|
|
|
76
77
|
pi-session-mcp-setup --verify --target claude-code:user:pi-session-mcp
|
|
77
78
|
```
|
|
78
79
|
|
|
79
|
-
Use `codex:user:pi-session-mcp` for Codex. The example configuration contains placeholders only: replace the workspace, provider and model with real local values, and never add credentials to the file.
|
|
80
|
+
Use `codex:user:pi-session-mcp` for Codex. The example configuration contains placeholders only: replace the workspace, provider and model with real local values, and never add credentials to the file. The example also registers the two worker skills with `builtin:` references, which resolve to the skills in the package, so the copied example needs no path edits for skills (see [Pi skills](docs/skills.md)).
|
|
80
81
|
|
|
81
|
-
`
|
|
82
|
+
`--apply` registers the server and also installs the controller skill `pi-session-orchestration` for that client, so the agent knows how to drive Pi worker sessions: Claude Code `user` and `local` get `~/.claude/skills/`, Claude Code `project` gets `.claude/skills/` of the project, and Codex gets `~/.agents/skills/`. Setup never replaces a skill it did not install or that you modified. To adjust a skill, copy it into the override directory next to your configuration file and edit the copy:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pi-session-mcp-setup --apply --customize-skill pi-session-review
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
To add a skill of your own, create `skills/<name>/SKILL.md` next to the configuration file and reference it as `builtin:<name>`. Do not edit the installed package or the installed copy in the client.
|
|
89
|
+
|
|
90
|
+
`pi-session-mcp-doctor` is an offline health check. `pi-session-mcp-setup` plans by default, changes a client registration or skill only with an explicit operation, and never replaces an existing divergent registration or a skill it did not install. Neither needs credentials, a provider or a model.
|
|
82
91
|
|
|
83
92
|
The [installation guide](docs/installation.md) also covers installing from a release tag with the exact locked dependency tree, the configuration rules, existing registrations, configuration changes, upgrades and removal.
|
|
84
93
|
|
|
@@ -104,6 +113,7 @@ The [installation guide](docs/installation.md) also covers installing from a rel
|
|
|
104
113
|
- A workspace alias maps to a directory. Clients can select aliases only.
|
|
105
114
|
- An execution profile binds a permission profile (`read-only` or `coding`), a provider, a model and a thinking level. Exactly one profile is the default, and it must be `read-only`.
|
|
106
115
|
- A profile can grant individual tools of local stdio MCP servers to its sessions; see [External MCP tools in embedded sessions](docs/session-mcp.md).
|
|
116
|
+
- An optional top-level `skillCatalog` maps skill aliases to `builtin:<name>` (a skill of the package or of your override directory) or to a `SKILL.md` file, and a profile's optional `skills` selects aliases from it. A profile without `skills` gets no skills. See [Pi skills](docs/skills.md).
|
|
107
117
|
- The server reads the file once at startup. Start a new client session after changing it.
|
|
108
118
|
|
|
109
119
|
## Run it by hand
|
|
@@ -144,6 +154,7 @@ Operate:
|
|
|
144
154
|
- [Installation](docs/installation.md)
|
|
145
155
|
- [Client setup reference](docs/client-setup.md): manual registration, the tool flow, deadlines and troubleshooting
|
|
146
156
|
- [External MCP tools in embedded sessions](docs/session-mcp.md)
|
|
157
|
+
- [Pi skills in execution profiles](docs/skills.md)
|
|
147
158
|
- [Changelog](CHANGELOG.md)
|
|
148
159
|
|
|
149
160
|
Contract and design:
|
|
@@ -157,6 +168,7 @@ Contract and design:
|
|
|
157
168
|
- [ADR 0002: embedded Pi SDK](docs/adr/0002-embedded-pi-sdk.md)
|
|
158
169
|
- [ADR 0003: no new feature phase after the MVP](docs/adr/0003-post-mvp-maintenance.md)
|
|
159
170
|
- [ADR 0004: setup accepts clients by contract, not by version](docs/adr/0004-client-contract-not-version.md)
|
|
171
|
+
- [ADR 0007: execution profiles select Pi skills from an operator catalog](docs/adr/0007-profile-skill-selection.md)
|
|
160
172
|
|
|
161
173
|
Maintain:
|
|
162
174
|
|
package/dist/capabilities.d.ts
CHANGED
package/dist/capabilities.js
CHANGED
|
@@ -14,7 +14,7 @@ export function projectCapabilities(workspaces, executionProfiles, configuration
|
|
|
14
14
|
.map((alias) => ({ alias })),
|
|
15
15
|
executionProfiles: [...executionProfiles.values()]
|
|
16
16
|
.sort((left, right) => compareAliases(left.alias, right.alias))
|
|
17
|
-
.map(({ alias, default: isDefault, permissionProfile, provider, model, thinkingLevel, extensions }) => ({
|
|
17
|
+
.map(({ alias, default: isDefault, permissionProfile, provider, model, thinkingLevel, extensions, skills }) => ({
|
|
18
18
|
alias,
|
|
19
19
|
default: isDefault,
|
|
20
20
|
permissionProfile,
|
|
@@ -22,6 +22,7 @@ export function projectCapabilities(workspaces, executionProfiles, configuration
|
|
|
22
22
|
model,
|
|
23
23
|
thinkingLevel,
|
|
24
24
|
extensions: (extensions ?? []).map((extension) => extension.spec),
|
|
25
|
+
skills: (skills ?? []).map((skill) => skill.alias),
|
|
25
26
|
})),
|
|
26
27
|
};
|
|
27
28
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MAX_PROFILE_EXTENSIONS, parseExtensionSpec } from "../extension-spec.js";
|
|
2
|
+
import { MAX_PROFILE_SKILLS } from "../skill-catalog.js";
|
|
2
3
|
import { PACKAGE_NAME, PACKAGE_VERSION } from "../package-metadata.js";
|
|
3
4
|
export const MCP_VERIFY_TIMEOUT_MS = 5_000;
|
|
4
5
|
export const MCP_MAX_FRAME_BYTES = 65_536;
|
|
@@ -99,7 +100,7 @@ function validCapabilities(value) {
|
|
|
99
100
|
if (!profile || typeof profile !== "object")
|
|
100
101
|
return false;
|
|
101
102
|
const record = profile;
|
|
102
|
-
return exactKeys(record, ["alias", "default", "permissionProfile", "provider", "model", "thinkingLevel", "extensions"])
|
|
103
|
+
return exactKeys(record, ["alias", "default", "permissionProfile", "provider", "model", "thinkingLevel", "extensions", "skills"])
|
|
103
104
|
&& typeof record.alias === "string" && ALIAS_PATTERN.test(record.alias)
|
|
104
105
|
&& typeof record.default === "boolean"
|
|
105
106
|
&& (record.permissionProfile === "read-only" || record.permissionProfile === "coding")
|
|
@@ -107,7 +108,10 @@ function validCapabilities(value) {
|
|
|
107
108
|
&& typeof record.model === "string" && record.model.length > 0 && record.model.length <= 256
|
|
108
109
|
&& typeof record.thinkingLevel === "string" && ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(record.thinkingLevel)
|
|
109
110
|
&& Array.isArray(record.extensions) && record.extensions.length <= MAX_PROFILE_EXTENSIONS
|
|
110
|
-
&& record.extensions.every((spec) => typeof spec === "string" && parseExtensionSpec(spec) !== undefined)
|
|
111
|
+
&& record.extensions.every((spec) => typeof spec === "string" && parseExtensionSpec(spec) !== undefined)
|
|
112
|
+
&& Array.isArray(record.skills) && record.skills.length <= MAX_PROFILE_SKILLS
|
|
113
|
+
&& record.skills.every((skill) => typeof skill === "string" && ALIAS_PATTERN.test(skill))
|
|
114
|
+
&& new Set(record.skills).size === record.skills.length;
|
|
111
115
|
}))
|
|
112
116
|
return false;
|
|
113
117
|
const defaults = profiles.filter((profile) => profile.default === true);
|
package/dist/config.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare class ConfigError extends Error {
|
|
|
7
7
|
}
|
|
8
8
|
export declare const configSchema: z.ZodObject<{
|
|
9
9
|
workspaces: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
10
|
+
skillCatalog: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
10
11
|
executionProfiles: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
11
12
|
default: z.ZodBoolean;
|
|
12
13
|
permissionProfile: z.ZodEnum<{
|
|
@@ -26,6 +27,7 @@ export declare const configSchema: z.ZodObject<{
|
|
|
26
27
|
}>;
|
|
27
28
|
mcpServers: z.ZodOptional<z.ZodCustom<Readonly<Record<string, import("./mcp-config.js").McpServerConfig>>, Readonly<Record<string, import("./mcp-config.js").McpServerConfig>>>>;
|
|
28
29
|
extensions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
30
|
+
skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
29
31
|
}, z.core.$strict>>;
|
|
30
32
|
}, z.core.$strict>;
|
|
31
33
|
/** The only supported reload behavior: a running controller keeps the snapshot it loaded at startup. */
|
|
@@ -42,6 +44,8 @@ export interface AppConfig {
|
|
|
42
44
|
readonly executionProfiles: ReadonlyMap<string, ConfiguredExecutionProfile>;
|
|
43
45
|
readonly defaultExecutionProfile: string;
|
|
44
46
|
readonly configuration: ConfigurationMetadata;
|
|
47
|
+
/** Absolute skill override directory next to the configuration file. Internal; never projected. */
|
|
48
|
+
readonly skillOverrideDirectory: string;
|
|
45
49
|
}
|
|
46
50
|
/**
|
|
47
51
|
* The configuration path comes only from the caller or from `PI_SESSION_MCP_CONFIG`.
|
package/dist/config.js
CHANGED
|
@@ -4,16 +4,20 @@ import { dirname, resolve } from "node:path";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { MAX_PROFILE_EXTENSIONS, parseExtensionSpec } from "./extension-spec.js";
|
|
6
6
|
import { isMcpServers, snapshotMcpServers } from "./mcp-config.js";
|
|
7
|
+
import { builtinSkillName, isSkillCatalogPath, MAX_PROFILE_SKILLS, MAX_SKILL_CATALOG_ENTRIES } from "./skill-catalog.js";
|
|
7
8
|
import { ALIAS_PATTERN, ImmutableMap, } from "./execution-profile.js";
|
|
9
|
+
import { builtinSkillPath, skillOverrideDirectory } from "./skill-library.js";
|
|
8
10
|
const alias = z.string().regex(ALIAS_PATTERN);
|
|
9
11
|
const permissionProfile = z.enum(["read-only", "coding"]);
|
|
10
12
|
const thinkingLevel = z.enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
11
13
|
const extensionSpec = z.string().refine((value) => parseExtensionSpec(value) !== undefined);
|
|
14
|
+
const skillCatalogPath = z.string().refine(isSkillCatalogPath);
|
|
12
15
|
const profileSchema = z.object({
|
|
13
16
|
default: z.boolean(), permissionProfile, provider: z.string().trim().min(1).max(256),
|
|
14
17
|
model: z.string().trim().min(1).max(256), thinkingLevel,
|
|
15
18
|
mcpServers: z.custom((value) => isMcpServers(value)).optional(),
|
|
16
19
|
extensions: z.array(extensionSpec).max(MAX_PROFILE_EXTENSIONS).optional(),
|
|
20
|
+
skills: z.array(alias).max(MAX_PROFILE_SKILLS).optional(),
|
|
17
21
|
}).strict().superRefine((profile, context) => {
|
|
18
22
|
if (profile.mcpServers !== undefined && !isMcpServers(profile.mcpServers, profile.permissionProfile)) {
|
|
19
23
|
context.addIssue({ code: "custom", path: ["mcpServers"], message: "MCP tool grants do not match the permission profile" });
|
|
@@ -24,6 +28,9 @@ const profileSchema = z.object({
|
|
|
24
28
|
context.addIssue({ code: "custom", path: ["extensions"], message: "An extension package may appear only once per profile" });
|
|
25
29
|
}
|
|
26
30
|
}
|
|
31
|
+
if (profile.skills !== undefined && new Set(profile.skills).size !== profile.skills.length) {
|
|
32
|
+
context.addIssue({ code: "custom", path: ["skills"], message: "A skill alias may appear only once per profile" });
|
|
33
|
+
}
|
|
27
34
|
});
|
|
28
35
|
const CONFIG_ERROR_MESSAGES = {
|
|
29
36
|
config_path_required: "Configuration path is required",
|
|
@@ -40,6 +47,7 @@ export class ConfigError extends Error {
|
|
|
40
47
|
}
|
|
41
48
|
export const configSchema = z.object({
|
|
42
49
|
workspaces: z.record(alias, z.string().min(1)),
|
|
50
|
+
skillCatalog: z.record(alias, skillCatalogPath).refine((catalog) => Object.keys(catalog).length <= MAX_SKILL_CATALOG_ENTRIES).optional(),
|
|
43
51
|
executionProfiles: z.record(alias, profileSchema).superRefine((profiles, context) => {
|
|
44
52
|
const entries = Object.entries(profiles);
|
|
45
53
|
if (entries.length === 0)
|
|
@@ -50,13 +58,39 @@ export const configSchema = z.object({
|
|
|
50
58
|
if (defaults.length === 1 && defaults[0][1].permissionProfile !== "read-only")
|
|
51
59
|
context.addIssue({ code: "custom", message: "The default execution profile must be read-only" });
|
|
52
60
|
}),
|
|
53
|
-
}).strict()
|
|
61
|
+
}).strict().superRefine((config, context) => {
|
|
62
|
+
// A profile may select only cataloged skills; there is no implicit or default skill.
|
|
63
|
+
const catalog = config.skillCatalog ?? {};
|
|
64
|
+
for (const [profileAlias, profile] of Object.entries(config.executionProfiles)) {
|
|
65
|
+
if ((profile.skills ?? []).some((skill) => !Object.hasOwn(catalog, skill))) {
|
|
66
|
+
context.addIssue({ code: "custom", path: ["executionProfiles", profileAlias, "skills"], message: "A selected skill must be a skillCatalog alias" });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
54
70
|
/** The only supported reload behavior: a running controller keeps the snapshot it loaded at startup. */
|
|
55
71
|
export const RELOAD_POLICY = "restart-required";
|
|
56
72
|
export const CONFIGURATION_FINGERPRINT_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
57
73
|
function configurationFingerprint(bytes) {
|
|
58
74
|
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
59
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolves a profile's selection against the catalog. Catalog paths resolve like workspaces,
|
|
78
|
+
* against the configuration file's directory. Two selected aliases on the same path would
|
|
79
|
+
* load one skill twice under different public names, so they invalidate the configuration.
|
|
80
|
+
* A `builtin:<name>` value resolves to the library skill: the operator's override, else the shipped one.
|
|
81
|
+
*/
|
|
82
|
+
function resolveSkillSelection(selected, catalog, base, overrideDirectory) {
|
|
83
|
+
if (selected === undefined || selected.length === 0)
|
|
84
|
+
return undefined;
|
|
85
|
+
const resolveEntry = (value) => {
|
|
86
|
+
const builtin = builtinSkillName(value);
|
|
87
|
+
return builtin === undefined ? resolve(base, value) : builtinSkillPath(builtin, { overrideDirectory });
|
|
88
|
+
};
|
|
89
|
+
const skills = selected.map((skill) => Object.freeze({ alias: skill, path: resolveEntry(catalog[skill]) }));
|
|
90
|
+
if (new Set(skills.map((skill) => skill.path)).size !== skills.length)
|
|
91
|
+
throw new ConfigError("invalid_execution_profile_config");
|
|
92
|
+
return Object.freeze(skills);
|
|
93
|
+
}
|
|
60
94
|
/**
|
|
61
95
|
* The configuration path comes only from the caller or from `PI_SESSION_MCP_CONFIG`.
|
|
62
96
|
* There is no default file: a server's working directory is the client's project,
|
|
@@ -85,14 +119,20 @@ export async function loadConfig(path = process.env.PI_SESSION_MCP_CONFIG) {
|
|
|
85
119
|
throw error;
|
|
86
120
|
}
|
|
87
121
|
const base = dirname(absoluteConfig);
|
|
122
|
+
const overrideDirectory = skillOverrideDirectory(absoluteConfig);
|
|
88
123
|
const workspaces = new ImmutableMap(Object.entries(parsed.workspaces).map(([key, value]) => [key, resolve(base, value)]));
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
124
|
+
const catalog = parsed.skillCatalog ?? {};
|
|
125
|
+
const executionProfiles = new ImmutableMap(Object.entries(parsed.executionProfiles).map(([key, { mcpServers, extensions, skills, ...value }]) => {
|
|
126
|
+
const selection = resolveSkillSelection(skills, catalog, base, overrideDirectory);
|
|
127
|
+
return [key, Object.freeze({
|
|
128
|
+
alias: key, ...value,
|
|
129
|
+
...(mcpServers === undefined ? {} : { mcpServers: snapshotMcpServers(mcpServers) }),
|
|
130
|
+
...(extensions === undefined ? {} : { extensions: Object.freeze(extensions.map((spec) => parseExtensionSpec(spec))) }),
|
|
131
|
+
...(selection === undefined ? {} : { skills: selection }),
|
|
132
|
+
})];
|
|
133
|
+
}));
|
|
94
134
|
const defaultEntry = [...executionProfiles].find(([, profile]) => profile.default);
|
|
95
135
|
if (!defaultEntry)
|
|
96
136
|
throw new Error("Execution profile configuration is invalid");
|
|
97
|
-
return { workspaces, executionProfiles, defaultExecutionProfile: defaultEntry[0], configuration };
|
|
137
|
+
return { workspaces, executionProfiles, defaultExecutionProfile: defaultEntry[0], configuration, skillOverrideDirectory: overrideDirectory };
|
|
98
138
|
}
|
package/dist/doctor.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type AppConfig } from "./config.js";
|
|
|
2
2
|
import { type ExtensionSpec } from "./extension-spec.js";
|
|
3
3
|
export type DoctorSeverity = "ok" | "warning" | "error";
|
|
4
4
|
export type DoctorExitCode = 0 | 1 | 2 | 64;
|
|
5
|
-
export type DoctorCheckId = "node_version_unsupported" | "package_metadata_unreadable" | "build_missing" | "build_stale" | "version_mismatch" | "config_missing" | "config_unreadable" | "config_invalid" | "workspace_missing" | "workspace_not_directory" | "workspace_unreadable" | "workspace_not_writable" | "extension_missing" | "extension_version_mismatch";
|
|
5
|
+
export type DoctorCheckId = "node_version_unsupported" | "package_metadata_unreadable" | "build_missing" | "build_stale" | "version_mismatch" | "config_missing" | "config_unreadable" | "config_invalid" | "workspace_missing" | "workspace_not_directory" | "workspace_unreadable" | "workspace_not_writable" | "extension_missing" | "extension_version_mismatch" | "skill_missing" | "skill_unreadable" | "skill_duplicate" | "skill_override_outdated";
|
|
6
6
|
export interface DoctorCheck {
|
|
7
7
|
readonly id: DoctorCheckId;
|
|
8
8
|
readonly severity: DoctorSeverity;
|
|
@@ -30,8 +30,11 @@ export interface DoctorFileSystem {
|
|
|
30
30
|
readonly isDirectory: () => boolean;
|
|
31
31
|
readonly isFile: () => boolean;
|
|
32
32
|
readonly mtimeMs?: number;
|
|
33
|
+
readonly size?: number;
|
|
33
34
|
}>;
|
|
34
35
|
readonly access: (path: string, mode: number) => Promise<void>;
|
|
36
|
+
/** Canonical path; defaults to the real file system when an injected fake omits it. */
|
|
37
|
+
readonly realpath?: (path: string) => Promise<string>;
|
|
35
38
|
}
|
|
36
39
|
export interface DoctorDependencies {
|
|
37
40
|
readonly packageRoot?: string;
|
package/dist/doctor.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { access, readFile, readdir, stat } from "node:fs/promises";
|
|
1
|
+
import { access, readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { loadConfig } from "./config.js";
|
|
5
5
|
import { PiExtensionError } from "./extension-spec.js";
|
|
6
6
|
import { readPackageMetadata } from "./package-metadata.js";
|
|
7
|
+
import { MAX_SKILL_FILE_BYTES } from "./skill-catalog.js";
|
|
8
|
+
import { readSkillBases, SkillBasesError } from "./skill-customize.js";
|
|
9
|
+
import { readSkillTree, shippedSkillsDirectory } from "./skill-library.js";
|
|
7
10
|
const defaultFileSystem = { readFile, stat, access };
|
|
8
|
-
const CHECK_ORDER = ["node_version_unsupported", "package_metadata_unreadable", "build_missing", "build_stale", "version_mismatch", "config_missing", "config_unreadable", "config_invalid", "workspace_missing", "workspace_not_directory", "workspace_unreadable", "workspace_not_writable", "extension_missing", "extension_version_mismatch"];
|
|
11
|
+
const CHECK_ORDER = ["node_version_unsupported", "package_metadata_unreadable", "build_missing", "build_stale", "version_mismatch", "config_missing", "config_unreadable", "config_invalid", "workspace_missing", "workspace_not_directory", "workspace_unreadable", "workspace_not_writable", "extension_missing", "extension_version_mismatch", "skill_missing", "skill_unreadable", "skill_duplicate", "skill_override_outdated"];
|
|
9
12
|
function failure(id, subject, remediation) { return Object.freeze({ id, severity: "error", subject, remediation }); }
|
|
10
13
|
function warning(id, subject, remediation) { return Object.freeze({ id, severity: "warning", subject, remediation }); }
|
|
11
14
|
function ok(id, subject) { return Object.freeze({ id, severity: "ok", subject }); }
|
|
@@ -62,6 +65,37 @@ async function productionBuildInputMtimes(root) {
|
|
|
62
65
|
}
|
|
63
66
|
return mtimes;
|
|
64
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Compares the shipped skills with the fingerprints recorded when the operator copied them.
|
|
70
|
+
* Reads only the bases file and the shipped files; nothing is executed or changed.
|
|
71
|
+
*/
|
|
72
|
+
async function skillOverrideChecks(overrideDirectory, shippedDirectory) {
|
|
73
|
+
if (overrideDirectory === undefined)
|
|
74
|
+
return [];
|
|
75
|
+
let bases;
|
|
76
|
+
try {
|
|
77
|
+
bases = await readSkillBases(overrideDirectory);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error instanceof SkillBasesError)
|
|
81
|
+
return [warning("skill_override_outdated", "skill:bases", "repair or remove the skill bases file")];
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
const checks = [];
|
|
85
|
+
for (const [name, base] of Object.entries(bases).sort(([a], [b]) => a.localeCompare(b))) {
|
|
86
|
+
let fingerprint;
|
|
87
|
+
try {
|
|
88
|
+
fingerprint = (await readSkillTree(join(shippedDirectory, name))).fingerprint;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
checks.push(fingerprint === base
|
|
94
|
+
? ok("skill_override_outdated", `skill:${name}`)
|
|
95
|
+
: warning("skill_override_outdated", `skill:${name}`, `review the changed shipped skill, then run pi-session-mcp-setup --apply --customize-skill ${name}`));
|
|
96
|
+
}
|
|
97
|
+
return checks;
|
|
98
|
+
}
|
|
65
99
|
export async function runDoctor(dependencies = {}) {
|
|
66
100
|
const fs = dependencies.fileSystem ?? defaultFileSystem;
|
|
67
101
|
const root = dependencies.packageRoot ?? process.cwd();
|
|
@@ -195,6 +229,38 @@ export async function runDoctor(dependencies = {}) {
|
|
|
195
229
|
}
|
|
196
230
|
}
|
|
197
231
|
}
|
|
232
|
+
if (config) {
|
|
233
|
+
for (const [alias, profile] of [...config.executionProfiles].sort(([a], [b]) => a.localeCompare(b))) {
|
|
234
|
+
const canonical = new Set();
|
|
235
|
+
for (const skill of profile.skills ?? []) {
|
|
236
|
+
const subject = `${alias}:${skill.alias}`;
|
|
237
|
+
let real;
|
|
238
|
+
try {
|
|
239
|
+
const info = await fs.stat(skill.path);
|
|
240
|
+
if (!info.isFile() || (info.size ?? 0) > MAX_SKILL_FILE_BYTES)
|
|
241
|
+
throw new Error("unusable skill file");
|
|
242
|
+
await fs.access(skill.path, constants.R_OK);
|
|
243
|
+
real = await (fs.realpath ?? realpath)(skill.path);
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
if (isMissing(error))
|
|
247
|
+
checks.push(failure("skill_missing", subject, "create the skill file or fix its skillCatalog entry"));
|
|
248
|
+
else
|
|
249
|
+
checks.push(ok("skill_missing", subject), failure("skill_unreadable", subject, "point the skillCatalog entry to a readable SKILL.md within the size limit"));
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
checks.push(ok("skill_missing", subject), ok("skill_unreadable", subject));
|
|
253
|
+
if (canonical.has(real))
|
|
254
|
+
checks.push(failure("skill_duplicate", subject, "select each skill file only once per profile"));
|
|
255
|
+
else {
|
|
256
|
+
canonical.add(real);
|
|
257
|
+
checks.push(ok("skill_duplicate", subject));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (config)
|
|
263
|
+
checks.push(...await skillOverrideChecks(config.skillOverrideDirectory, shippedSkillsDirectory(dependencies.packageRoot)));
|
|
198
264
|
const rank = new Map(CHECK_ORDER.map((id, index) => [id, index]));
|
|
199
265
|
checks.sort((a, b) => (rank.get(a.id) ?? 999) - (rank.get(b.id) ?? 999) || a.subject.localeCompare(b.subject));
|
|
200
266
|
const hasError = checks.some((check) => check.severity === "error");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionSpec } from "./extension-spec.js";
|
|
2
2
|
import { type McpServers } from "./mcp-config.js";
|
|
3
|
+
import type { SkillSelection } from "./skill-catalog.js";
|
|
3
4
|
export declare const ALIAS_PATTERN: RegExp;
|
|
4
5
|
export type PermissionProfile = "read-only" | "coding";
|
|
5
6
|
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
@@ -13,6 +14,8 @@ export interface ConfiguredExecutionProfile {
|
|
|
13
14
|
readonly thinkingLevel: ThinkingLevel;
|
|
14
15
|
readonly mcpServers?: McpServers;
|
|
15
16
|
readonly extensions?: readonly ExtensionSpec[];
|
|
17
|
+
/** The selected catalog skills in configured order; absent when the profile selects none. */
|
|
18
|
+
readonly skills?: readonly SkillSelection[];
|
|
16
19
|
}
|
|
17
20
|
/** The immutable, server-side selection passed to the Pi adapter. */
|
|
18
21
|
export interface ResolvedExecutionProfile {
|
|
@@ -23,6 +26,7 @@ export interface ResolvedExecutionProfile {
|
|
|
23
26
|
readonly thinkingLevel: ThinkingLevel;
|
|
24
27
|
readonly mcpServers?: McpServers;
|
|
25
28
|
readonly extensions?: readonly ExtensionSpec[];
|
|
29
|
+
readonly skills?: readonly SkillSelection[];
|
|
26
30
|
}
|
|
27
31
|
export declare function toResolvedExecutionProfile(profile: ConfiguredExecutionProfile): ResolvedExecutionProfile;
|
|
28
32
|
/** A small read-only map implementation which has no mutating methods at runtime. */
|
|
@@ -9,6 +9,7 @@ export function toResolvedExecutionProfile(profile) {
|
|
|
9
9
|
thinkingLevel: profile.thinkingLevel,
|
|
10
10
|
...(profile.mcpServers === undefined ? {} : { mcpServers: snapshotMcpServers(profile.mcpServers) }),
|
|
11
11
|
...(profile.extensions === undefined ? {} : { extensions: profile.extensions }),
|
|
12
|
+
...(profile.skills === undefined ? {} : { skills: profile.skills }),
|
|
12
13
|
});
|
|
13
14
|
}
|
|
14
15
|
/** A small read-only map implementation which has no mutating methods at runtime. */
|
package/dist/extension-spec.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export const MAX_PROFILE_EXTENSIONS = 8;
|
|
2
2
|
const MAX_SPEC_LENGTH = 256;
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* `npm:<name>@<exact semver>` with an optional prerelease; the name may be scoped. Ranges, tags,
|
|
5
|
+
* git sources and paths never match. Build metadata (`+build`) never matches either: npm does not
|
|
6
|
+
* keep it in a published version, so such a pin could never equal the installed manifest. Rejecting
|
|
7
|
+
* it here fails the configuration instead of every session with `extension_version_mismatch`.
|
|
8
|
+
*/
|
|
9
|
+
const EXTENSION_SPEC_PATTERN = /^npm:((?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*)@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/;
|
|
5
10
|
export function parseExtensionSpec(spec) {
|
|
6
11
|
if (spec.length > MAX_SPEC_LENGTH)
|
|
7
12
|
return undefined;
|
package/dist/pi-adapter.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface PiAppliedSelection {
|
|
|
13
13
|
readonly model: string;
|
|
14
14
|
readonly thinkingLevel: ThinkingLevel;
|
|
15
15
|
}
|
|
16
|
-
export type PiSessionCreationErrorCode = "unknown_provider" | "unknown_model" | "local_authentication_missing" | "local_authentication_unavailable" | "model_unavailable" | "thinking_level_unavailable" | "execution_selection_mismatch" | "external_mcp_unavailable" | "external_mcp_no_tools" | "external_mcp_grant_tool_missing" | "external_mcp_activation_mismatch" | "extension_not_installed" | "extension_version_mismatch" | "extension_load_failed" | "extension_tool_conflict" | "pi_session_creation_failed";
|
|
16
|
+
export type PiSessionCreationErrorCode = "unknown_provider" | "unknown_model" | "local_authentication_missing" | "local_authentication_unavailable" | "model_unavailable" | "thinking_level_unavailable" | "execution_selection_mismatch" | "external_mcp_unavailable" | "external_mcp_no_tools" | "external_mcp_grant_tool_missing" | "external_mcp_activation_mismatch" | "extension_not_installed" | "extension_version_mismatch" | "extension_load_failed" | "extension_tool_conflict" | "skill_load_failed" | "pi_session_creation_failed";
|
|
17
17
|
export declare class PiSessionCreationError extends Error {
|
|
18
18
|
readonly code: PiSessionCreationErrorCode;
|
|
19
19
|
constructor(code: PiSessionCreationErrorCode);
|
package/dist/pi-adapter.js
CHANGED
|
@@ -14,6 +14,7 @@ const PI_SESSION_CREATION_MESSAGES = {
|
|
|
14
14
|
extension_version_mismatch: "Configured Pi extension version is not installed",
|
|
15
15
|
extension_load_failed: "Configured Pi extension could not be loaded",
|
|
16
16
|
extension_tool_conflict: "Configured Pi extension tools conflict",
|
|
17
|
+
skill_load_failed: "Configured Pi skill could not be loaded",
|
|
17
18
|
pi_session_creation_failed: "Pi session creation failed",
|
|
18
19
|
};
|
|
19
20
|
export class PiSessionCreationError extends Error {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Stats } from "node:fs";
|
|
2
|
+
import { loadSkills, type Skill } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { PiSessionCreationError } from "./pi-adapter.js";
|
|
4
|
+
import { type SkillSelection } from "./skill-catalog.js";
|
|
5
|
+
export declare class PiSkillError extends PiSessionCreationError {
|
|
6
|
+
readonly alias?: string | undefined;
|
|
7
|
+
constructor(alias?: string | undefined);
|
|
8
|
+
}
|
|
9
|
+
interface SkillDependencies {
|
|
10
|
+
readonly stat: (path: string) => Promise<Stats>;
|
|
11
|
+
readonly access: (path: string, mode: number) => Promise<void>;
|
|
12
|
+
readonly realpath: (path: string) => Promise<string>;
|
|
13
|
+
readonly loadSkills: typeof loadSkills;
|
|
14
|
+
}
|
|
15
|
+
/** Load only the configured files, independently of package discovery and SDK deduplication. */
|
|
16
|
+
export declare function loadSelectedSkills(selection: readonly SkillSelection[], cwd: string, dependencies?: Partial<SkillDependencies>): Promise<Skill[]>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { loadSkills } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { PiSessionCreationError } from "./pi-adapter.js";
|
|
6
|
+
import { MAX_SKILL_FILE_BYTES } from "./skill-catalog.js";
|
|
7
|
+
export class PiSkillError extends PiSessionCreationError {
|
|
8
|
+
alias;
|
|
9
|
+
constructor(alias) {
|
|
10
|
+
super("skill_load_failed");
|
|
11
|
+
this.alias = alias;
|
|
12
|
+
this.name = "PiSkillError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Load only the configured files, independently of package discovery and SDK deduplication. */
|
|
16
|
+
export async function loadSelectedSkills(selection, cwd, dependencies = {}) {
|
|
17
|
+
if (selection.length === 0)
|
|
18
|
+
return [];
|
|
19
|
+
const fs = { stat, access, realpath, loadSkills, ...dependencies };
|
|
20
|
+
const paths = [];
|
|
21
|
+
const canonical = new Set();
|
|
22
|
+
try {
|
|
23
|
+
for (const selected of selection) {
|
|
24
|
+
try {
|
|
25
|
+
const path = resolve(selected.path);
|
|
26
|
+
const info = await fs.stat(path);
|
|
27
|
+
if (!info.isFile() || info.size > MAX_SKILL_FILE_BYTES)
|
|
28
|
+
throw new PiSkillError(selected.alias);
|
|
29
|
+
await fs.access(path, constants.R_OK);
|
|
30
|
+
const real = await fs.realpath(path);
|
|
31
|
+
if (canonical.has(real))
|
|
32
|
+
throw new PiSkillError(selected.alias);
|
|
33
|
+
canonical.add(real);
|
|
34
|
+
paths.push(path);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new PiSkillError(selected.alias);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const result = fs.loadSkills({ cwd, agentDir: cwd, skillPaths: paths, includeDefaults: false });
|
|
41
|
+
if (result.diagnostics.length > 0) {
|
|
42
|
+
const aliases = new Set(result.diagnostics.map((diagnostic) => {
|
|
43
|
+
const index = paths.indexOf(diagnostic.path ?? "");
|
|
44
|
+
return index < 0 ? undefined : selection[index].alias;
|
|
45
|
+
}));
|
|
46
|
+
throw new PiSkillError(aliases.size === 1 ? [...aliases][0] : undefined);
|
|
47
|
+
}
|
|
48
|
+
if (result.skills.length !== paths.length) {
|
|
49
|
+
const missing = selection.filter((_selected, index) => !result.skills.some((skill) => skill.filePath === paths[index]));
|
|
50
|
+
throw new PiSkillError(missing.length === 1 ? missing[0].alias : undefined);
|
|
51
|
+
}
|
|
52
|
+
return paths.map((path, index) => {
|
|
53
|
+
const matches = result.skills.filter((skill) => skill.filePath === path);
|
|
54
|
+
if (matches.length !== 1 || matches[0].disableModelInvocation === true)
|
|
55
|
+
throw new PiSkillError(selection[index].alias);
|
|
56
|
+
return matches[0];
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error instanceof PiSkillError)
|
|
61
|
+
throw error;
|
|
62
|
+
throw new PiSkillError();
|
|
63
|
+
}
|
|
64
|
+
}
|