pi-session-mcp 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi-session-mcp-cli/config.js +47 -7
- package/.pi-session-mcp-cli/doctor.js +75 -2
- package/.pi-session-mcp-cli/execution-profile.js +1 -0
- package/.pi-session-mcp-cli/override-lock.js +125 -0
- package/.pi-session-mcp-cli/setup-platform.js +195 -0
- 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 +268 -0
- package/.pi-session-mcp-cli/skill-library.js +188 -0
- package/CHANGELOG.md +176 -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 +7 -1
- package/dist/doctor.js +75 -2
- package/dist/execution-profile.d.ts +4 -0
- package/dist/execution-profile.js +1 -0
- package/dist/override-lock.d.ts +34 -0
- package/dist/override-lock.js +125 -0
- package/dist/pi-adapter.d.ts +7 -5
- 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 +82 -16
- package/dist/server.js +1 -0
- package/dist/session-registry.d.ts +8 -6
- package/dist/session-registry.js +3 -2
- 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 +9 -0
- package/dist/setup-ownership.js +32 -5
- package/dist/setup-skills.d.ts +76 -0
- package/dist/setup-skills.js +586 -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 +41 -0
- package/dist/skill-customize.js +268 -0
- package/dist/skill-library.d.ts +106 -0
- package/dist/skill-library.js +188 -0
- package/docs/acceptance.md +593 -1
- package/docs/adr/0006-pi-extension-allowlist.md +3 -0
- package/docs/adr/0007-profile-skill-selection.md +228 -0
- package/docs/architecture.md +10 -6
- package/docs/client-setup-design.md +219 -1
- package/docs/client-setup.md +86 -3
- package/docs/doctor-design.md +9 -1
- package/docs/installation.md +128 -5
- package/docs/pi-extensions-design.md +28 -4
- package/docs/releasing.md +18 -9
- package/docs/session-mcp.md +7 -2
- package/docs/skills.md +616 -0
- package/docs/threat-model.md +19 -2
- package/docs/tool-contracts.md +9 -5
- package/package.json +2 -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 +94 -0
- package/skills/pi-session-review/SKILL.md +58 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { statSync } from "node:fs";
|
|
3
|
+
import { lstat, readdir, readFile, realpath } from "node:fs/promises";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
/**
|
|
7
|
+
* The skill library: skills shipped in the package's `skills/` directory, and the operator's
|
|
8
|
+
* override directory `skills/` next to the configuration file. An override with the same name
|
|
9
|
+
* replaces the shipped skill; a new name adds a skill. Nothing else is searched.
|
|
10
|
+
*/
|
|
11
|
+
/** Agent Skills name rule: lowercase letters and digits in hyphen-separated groups, 1 to 64 characters. */
|
|
12
|
+
export const SKILL_NAME_PATTERN = /^(?=.{1,64}$)[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
13
|
+
/** A `skillCatalog` value `builtin:<name>` names a library skill instead of a path. */
|
|
14
|
+
export const BUILTIN_SKILL_PREFIX = "builtin:";
|
|
15
|
+
/** Skills for the controlling agent, which setup installs into Claude Code and Codex. */
|
|
16
|
+
export const CONTROLLER_SKILLS = Object.freeze(["pi-session-orchestration"]);
|
|
17
|
+
export const MAX_SKILL_TREE_FILES = 32;
|
|
18
|
+
export const MAX_SKILL_TREE_BYTES = 1024 * 1024;
|
|
19
|
+
const MAX_SKILL_TREE_DEPTH = 8;
|
|
20
|
+
/** `dist/`, `.pi-session-mcp-cli/` and `src/` all sit one level below the package root. */
|
|
21
|
+
export const PACKAGE_ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
22
|
+
export function shippedSkillsDirectory(packageRoot = PACKAGE_ROOT) {
|
|
23
|
+
return join(packageRoot, "skills");
|
|
24
|
+
}
|
|
25
|
+
/** The override directory belongs to the configuration, never to a workspace or the shell's directory. */
|
|
26
|
+
export function skillOverrideDirectory(configPath) {
|
|
27
|
+
return join(dirname(resolve(configPath)), "skills");
|
|
28
|
+
}
|
|
29
|
+
function isRegularFile(path) {
|
|
30
|
+
try {
|
|
31
|
+
return statSync(path).isFile();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** The override wins over the shipped skill of the same name; undefined when neither exists. */
|
|
38
|
+
export function resolveLibrarySkill(name, roots) {
|
|
39
|
+
if (!SKILL_NAME_PATTERN.test(name))
|
|
40
|
+
return undefined;
|
|
41
|
+
const override = join(roots.overrideDirectory, name);
|
|
42
|
+
if (isRegularFile(join(override, "SKILL.md")))
|
|
43
|
+
return Object.freeze({ name, source: "override", directory: override });
|
|
44
|
+
const shipped = join(shippedSkillsDirectory(roots.packageRoot), name);
|
|
45
|
+
if (isRegularFile(join(shipped, "SKILL.md")))
|
|
46
|
+
return Object.freeze({ name, source: "shipped", directory: shipped });
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
/** The `SKILL.md` a `builtin:<name>` catalog value stands for; a missing skill keeps the shipped location. */
|
|
50
|
+
export function builtinSkillPath(name, roots) {
|
|
51
|
+
const skill = resolveLibrarySkill(name, roots);
|
|
52
|
+
return join(skill?.directory ?? join(shippedSkillsDirectory(roots.packageRoot), name), "SKILL.md");
|
|
53
|
+
}
|
|
54
|
+
export class SkillTreeError extends Error {
|
|
55
|
+
code;
|
|
56
|
+
constructor(code) {
|
|
57
|
+
super("Skill directory is not usable");
|
|
58
|
+
this.code = code;
|
|
59
|
+
this.name = "SkillTreeError";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** A version 2 skill tree fingerprint as it is stored. */
|
|
63
|
+
export const SKILL_TREE_FINGERPRINT_PATTERN = /^v2:[0-9a-f]{64}$/;
|
|
64
|
+
/** A version 1 fingerprint as 0.8.0 stored it: bare SHA-256 hex. */
|
|
65
|
+
export const LEGACY_SKILL_TREE_FINGERPRINT_PATTERN = /^[0-9a-f]{64}$/;
|
|
66
|
+
const FINGERPRINT_DOMAIN = "pi-session-mcp skill tree v2\n";
|
|
67
|
+
function uint32(value) {
|
|
68
|
+
const bytes = Buffer.alloc(4);
|
|
69
|
+
bytes.writeUInt32BE(value);
|
|
70
|
+
return bytes;
|
|
71
|
+
}
|
|
72
|
+
function uint64(value) {
|
|
73
|
+
const bytes = Buffer.alloc(8);
|
|
74
|
+
bytes.writeBigUInt64BE(BigInt(value));
|
|
75
|
+
return bytes;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Version 2: a domain and version tag, the file count, then per file in byte order of the path
|
|
79
|
+
* the length-prefixed UTF-8 path and the length-prefixed content. Every field has an explicit
|
|
80
|
+
* length, so two different trees never share an encoding.
|
|
81
|
+
*/
|
|
82
|
+
export function skillTreeFingerprint(files) {
|
|
83
|
+
const entries = files.map((file) => ({ path: Buffer.from(file.path, "utf8"), bytes: file.bytes }))
|
|
84
|
+
.sort((left, right) => Buffer.compare(left.path, right.path));
|
|
85
|
+
const hash = createHash("sha256").update(FINGERPRINT_DOMAIN, "utf8").update(uint32(entries.length));
|
|
86
|
+
entries.forEach((entry, index) => {
|
|
87
|
+
if (index > 0 && Buffer.compare(entries[index - 1].path, entry.path) === 0)
|
|
88
|
+
throw new SkillTreeError("skill_tree_invalid");
|
|
89
|
+
hash.update(uint32(entry.path.length)).update(entry.path).update(uint64(entry.bytes.length)).update(entry.bytes);
|
|
90
|
+
});
|
|
91
|
+
return `v2:${hash.digest("hex")}`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Version 1, as 0.8.0 computed it: `path NUL content NUL` per file in reading order. File
|
|
95
|
+
* content may contain NUL bytes, so two different trees can share this value.
|
|
96
|
+
*/
|
|
97
|
+
export function legacySkillTreeFingerprint(files) {
|
|
98
|
+
const hash = createHash("sha256");
|
|
99
|
+
for (const file of files)
|
|
100
|
+
hash.update(file.path).update("\0").update(file.bytes).update("\0");
|
|
101
|
+
return hash.digest("hex");
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The shipped skills of every release that recorded version 1 fingerprints (0.8.0 only), as
|
|
105
|
+
* trusted references for recognizing such records. Computed from the `v0.8.0` tag and checked
|
|
106
|
+
* against the published npm package.
|
|
107
|
+
*/
|
|
108
|
+
export const RELEASED_SKILL_TREES = Object.freeze([
|
|
109
|
+
Object.freeze({ name: "pi-session-orchestration", release: "0.8.0", legacyFingerprint: "e2a72b558ea14748a4badd0058ff65b8731cc125033a666706b103842784673a", fingerprint: "v2:39edc4c4783dec7f90345b22844a17e5e48b465722bb0f1b20059d6ee51e1523" }),
|
|
110
|
+
Object.freeze({ name: "pi-session-implementation", release: "0.8.0", legacyFingerprint: "5f719b44da266d3f66dd5bf3c18f3e3e04ca7df357e2fe2eedbf0409e6c1bf64", fingerprint: "v2:f8ce75eff83a5c952f6ca51c04e1e8025cb7c2c558c30d692f291a3f6594eb12" }),
|
|
111
|
+
Object.freeze({ name: "pi-session-review", release: "0.8.0", legacyFingerprint: "8cf58ccb8d473a3eedf12ce6b902b1e91dd9e2fef83ad110c4d8e2e6164cb257", fingerprint: "v2:610c0b4829eeb1ce6c9b2000ad73f8e88ca230707eef7f7b1708a80767952bf0" }),
|
|
112
|
+
]);
|
|
113
|
+
/**
|
|
114
|
+
* Whether a version 1 fingerprint from a record proves that `candidate` is the tree it names:
|
|
115
|
+
* a trusted reference, a released shipped tree of this name or one of `references` (such as
|
|
116
|
+
* the current library tree), has exactly this version 1 fingerprint and is byte-identical to
|
|
117
|
+
* `candidate` by its version 2 fingerprint. The version 1 fingerprint alone never suffices.
|
|
118
|
+
*/
|
|
119
|
+
export function provesLegacyFingerprint(name, legacyFingerprint, candidate, references = [], released = RELEASED_SKILL_TREES) {
|
|
120
|
+
if (!LEGACY_SKILL_TREE_FINGERPRINT_PATTERN.test(legacyFingerprint) || !SKILL_TREE_FINGERPRINT_PATTERN.test(candidate.fingerprint))
|
|
121
|
+
return false;
|
|
122
|
+
const trusted = [...released.filter((tree) => tree.name === name), ...references];
|
|
123
|
+
return trusted.some((tree) => tree.legacyFingerprint === legacyFingerprint && tree.fingerprint === candidate.fingerprint);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The version 2 fingerprint of the released shipped skill that a version 1 fingerprint names,
|
|
127
|
+
* or undefined. Only released trees qualify: the current shipped tree could share a version 1
|
|
128
|
+
* fingerprint with an older original without being that original.
|
|
129
|
+
*/
|
|
130
|
+
export function releasedSkillFingerprint(name, legacyFingerprint, released = RELEASED_SKILL_TREES) {
|
|
131
|
+
return released.find((tree) => tree.name === name && tree.legacyFingerprint === legacyFingerprint)?.fingerprint;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Reads the regular files below a skill directory in a stable order. Entries whose name starts
|
|
135
|
+
* with `.` are skipped or rejected (see options). A symbolic link or special file inside the
|
|
136
|
+
* tree, a missing `SKILL.md`, or more than the file and byte limits fail, so the tree can be
|
|
137
|
+
* copied and compared exactly.
|
|
138
|
+
*/
|
|
139
|
+
export async function readSkillTree(directory, options = {}) {
|
|
140
|
+
let root;
|
|
141
|
+
try {
|
|
142
|
+
root = await realpath(directory);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
throw new SkillTreeError("skill_tree_missing");
|
|
146
|
+
}
|
|
147
|
+
const files = [];
|
|
148
|
+
let bytes = 0;
|
|
149
|
+
const visit = async (current, prefix, depth) => {
|
|
150
|
+
if (depth > MAX_SKILL_TREE_DEPTH)
|
|
151
|
+
throw new SkillTreeError("skill_tree_invalid");
|
|
152
|
+
let entries;
|
|
153
|
+
try {
|
|
154
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
throw new SkillTreeError("skill_tree_invalid");
|
|
158
|
+
}
|
|
159
|
+
for (const entry of entries.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0))) {
|
|
160
|
+
if (entry.name.startsWith(".")) {
|
|
161
|
+
if (options.hidden === "reject")
|
|
162
|
+
throw new SkillTreeError("skill_tree_invalid");
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const path = join(current, entry.name);
|
|
166
|
+
const relativePath = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
167
|
+
const info = await lstat(path);
|
|
168
|
+
if (info.isDirectory()) {
|
|
169
|
+
await visit(path, relativePath, depth + 1);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (!info.isFile())
|
|
173
|
+
throw new SkillTreeError("skill_tree_invalid");
|
|
174
|
+
if (files.length + 1 > MAX_SKILL_TREE_FILES || bytes + info.size > MAX_SKILL_TREE_BYTES)
|
|
175
|
+
throw new SkillTreeError("skill_tree_too_large");
|
|
176
|
+
const content = await readFile(path);
|
|
177
|
+
// Counted as read, so a file that grew after lstat cannot pass the limit.
|
|
178
|
+
bytes += content.length;
|
|
179
|
+
if (bytes > MAX_SKILL_TREE_BYTES)
|
|
180
|
+
throw new SkillTreeError("skill_tree_too_large");
|
|
181
|
+
files.push(Object.freeze({ path: relativePath, bytes: content }));
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
await visit(root, "", 0);
|
|
185
|
+
if (!files.some((file) => file.path === "SKILL.md"))
|
|
186
|
+
throw new SkillTreeError("skill_tree_missing");
|
|
187
|
+
return Object.freeze({ files: Object.freeze(files), fingerprint: skillTreeFingerprint(files), legacyFingerprint: legacySkillTreeFingerprint(files) });
|
|
188
|
+
}
|