pi-session-mcp 0.7.1 → 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/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 +85 -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/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 +54 -11
- package/dist/server.js +1 -0
- 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 +243 -1
- package/docs/adr/0006-pi-extension-allowlist.md +3 -0
- 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 +122 -5
- package/docs/pi-extensions-design.md +17 -4
- package/docs/session-mcp.md +7 -2
- package/docs/skills.md +487 -0
- package/docs/threat-model.md +19 -2
- package/docs/tool-contracts.md +8 -4
- 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 +93 -0
- package/skills/pi-session-review/SKILL.md +58 -0
package/dist/sdk-pi-adapter.js
CHANGED
|
@@ -4,6 +4,7 @@ import { classifyMcpStartupError, connectSessionMcpTools, } from "./mcp-session-
|
|
|
4
4
|
import { createStdioMcpConnection } from "./mcp-stdio-client.js";
|
|
5
5
|
import { PiSessionCreationError, } from "./pi-adapter.js";
|
|
6
6
|
import { resolvePiExtension } from "./pi-extensions.js";
|
|
7
|
+
import { loadSelectedSkills, PiSkillError } from "./pi-skills.js";
|
|
7
8
|
import { composeSessionTools, PROFILE_TOOLS } from "./session-tools.js";
|
|
8
9
|
export { PROFILE_TOOLS } from "./session-tools.js";
|
|
9
10
|
export const AUTH_OPERATION_TIMEOUT_MS = 5_000;
|
|
@@ -269,8 +270,10 @@ function isTextBlock(block) {
|
|
|
269
270
|
const candidate = block;
|
|
270
271
|
return candidate.type === "text" && typeof candidate.text === "string";
|
|
271
272
|
}
|
|
272
|
-
/**
|
|
273
|
-
function phase0ResourceLoaderOptions() {
|
|
273
|
+
/** Independently validated skills replace package and extension discovery on every reload. */
|
|
274
|
+
function phase0ResourceLoaderOptions(skills) {
|
|
275
|
+
// Keep the closure's snapshot private: the SDK may annotate or mutate returned objects.
|
|
276
|
+
const selected = skills.map((skill) => ({ ...skill, sourceInfo: { ...skill.sourceInfo } }));
|
|
274
277
|
return {
|
|
275
278
|
noExtensions: true,
|
|
276
279
|
noSkills: true,
|
|
@@ -281,11 +284,14 @@ function phase0ResourceLoaderOptions() {
|
|
|
281
284
|
appendSystemPrompt: [],
|
|
282
285
|
systemPromptOverride: () => undefined,
|
|
283
286
|
appendSystemPromptOverride: () => [],
|
|
287
|
+
skillsOverride: () => ({ skills: selected.map((skill) => ({ ...skill, sourceInfo: { ...skill.sourceInfo } })), diagnostics: [] }),
|
|
288
|
+
promptsOverride: () => ({ prompts: [], diagnostics: [] }),
|
|
289
|
+
themesOverride: () => ({ themes: [], diagnostics: [] }),
|
|
284
290
|
};
|
|
285
291
|
}
|
|
286
|
-
export async function createPhase0ResourceLoader(cwd) {
|
|
292
|
+
export async function createPhase0ResourceLoader(cwd, skills = []) {
|
|
287
293
|
const settingsManager = SettingsManager.inMemory(undefined, { projectTrusted: false });
|
|
288
|
-
const resourceLoader = new DefaultResourceLoader({ cwd, agentDir: cwd, settingsManager, ...phase0ResourceLoaderOptions() });
|
|
294
|
+
const resourceLoader = new DefaultResourceLoader({ cwd, agentDir: cwd, settingsManager, ...phase0ResourceLoaderOptions(skills) });
|
|
289
295
|
await resourceLoader.reload();
|
|
290
296
|
return { resourceLoader, settingsManager };
|
|
291
297
|
}
|
|
@@ -310,6 +316,7 @@ export class SdkPiSessionAdapter {
|
|
|
310
316
|
if (extensions.length > 0)
|
|
311
317
|
return this.createWithExtensions(input, extensions);
|
|
312
318
|
const { executionProfile } = input;
|
|
319
|
+
const skills = await this.loadSkills(input);
|
|
313
320
|
let runtime;
|
|
314
321
|
try {
|
|
315
322
|
runtime = await this.getRuntime();
|
|
@@ -321,7 +328,7 @@ export class SdkPiSessionAdapter {
|
|
|
321
328
|
let resourceLoader;
|
|
322
329
|
let settingsManager;
|
|
323
330
|
try {
|
|
324
|
-
({ resourceLoader, settingsManager } = await createPhase0ResourceLoader(input.cwd));
|
|
331
|
+
({ resourceLoader, settingsManager } = await createPhase0ResourceLoader(input.cwd, skills));
|
|
325
332
|
}
|
|
326
333
|
catch {
|
|
327
334
|
throw this.failure(executionProfile, "resource_loader", "pi_session_creation_failed");
|
|
@@ -347,10 +354,11 @@ export class SdkPiSessionAdapter {
|
|
|
347
354
|
await mcpTools.close();
|
|
348
355
|
throw this.failure(executionProfile, "create_session", "pi_session_creation_failed");
|
|
349
356
|
}
|
|
350
|
-
return this.admit(session, executionProfile, tools, mcpTools, () => disposeSdkSession(session), mcpTools.tools.length > 0 ? "external_mcp_activation_mismatch" : undefined);
|
|
357
|
+
return this.admit(session, executionProfile, tools, mcpTools, () => disposeSdkSession(session), mcpTools.tools.length > 0 ? "external_mcp_activation_mismatch" : undefined, "exact", skills);
|
|
351
358
|
}
|
|
352
359
|
async createWithExtensions(input, extensions) {
|
|
353
360
|
const { cwd, executionProfile } = input;
|
|
361
|
+
const skills = await this.loadSkills(input);
|
|
354
362
|
const directories = [];
|
|
355
363
|
for (const extension of extensions) {
|
|
356
364
|
try {
|
|
@@ -377,15 +385,18 @@ export class SdkPiSessionAdapter {
|
|
|
377
385
|
agentDir: cwd,
|
|
378
386
|
modelRuntime: runtime,
|
|
379
387
|
settingsManager: SettingsManager.inMemory(undefined, { projectTrusted: false }),
|
|
380
|
-
resourceLoaderOptions: { ...phase0ResourceLoaderOptions(), additionalExtensionPaths: directories },
|
|
388
|
+
resourceLoaderOptions: { ...phase0ResourceLoaderOptions(skills), additionalExtensionPaths: directories },
|
|
381
389
|
}));
|
|
390
|
+
this.checkResources(services.resourceLoader, skills, executionProfile, "extension_load");
|
|
382
391
|
const loaded = services.resourceLoader.getExtensions();
|
|
383
392
|
if (loaded.errors.length > 0 || services.diagnostics.some((diagnostic) => diagnostic.type === "error")) {
|
|
384
393
|
throw new Error("extension load reported errors");
|
|
385
394
|
}
|
|
386
395
|
extensionTools = registeredExtensionTools(services);
|
|
387
396
|
}
|
|
388
|
-
catch {
|
|
397
|
+
catch (error) {
|
|
398
|
+
if (error instanceof PiSessionCreationError)
|
|
399
|
+
throw error;
|
|
389
400
|
throw this.failure(executionProfile, "extension_load", "extension_load_failed");
|
|
390
401
|
}
|
|
391
402
|
const model = await this.resolveVerifiedModel(services.modelRuntime, executionProfile);
|
|
@@ -434,6 +445,7 @@ export class SdkPiSessionAdapter {
|
|
|
434
445
|
bound = true;
|
|
435
446
|
if (bindErrors > 0)
|
|
436
447
|
throw new Error("extension hook reported errors while binding");
|
|
448
|
+
this.checkResources(sessionRuntime.session.resourceLoader, skills, executionProfile, "extension_bind");
|
|
437
449
|
// A `session_start` handler may register a tool that replaces a built-in or granted tool.
|
|
438
450
|
// Tools an extension registers later, during a turn, are not re-checked: allowlisted
|
|
439
451
|
// extensions are trusted code.
|
|
@@ -441,15 +453,44 @@ export class SdkPiSessionAdapter {
|
|
|
441
453
|
if (!recheck || recheck.length !== tools.length || recheck.some((name, index) => name !== tools[index]))
|
|
442
454
|
conflict = true;
|
|
443
455
|
}
|
|
444
|
-
catch {
|
|
456
|
+
catch (error) {
|
|
445
457
|
await cleanUp();
|
|
458
|
+
if (error instanceof PiSessionCreationError)
|
|
459
|
+
throw error;
|
|
446
460
|
throw this.failure(executionProfile, "extension_bind", "extension_load_failed");
|
|
447
461
|
}
|
|
448
462
|
if (conflict) {
|
|
449
463
|
await cleanUp();
|
|
450
464
|
throw this.failure(executionProfile, "extension_bind", "extension_tool_conflict");
|
|
451
465
|
}
|
|
452
|
-
return this.admit(sessionRuntime.session, executionProfile, tools, mcpTools, disposeRuntime, mcpTools.tools.length > 0 ? "external_mcp_activation_mismatch" : "execution_selection_mismatch", "subset");
|
|
466
|
+
return this.admit(sessionRuntime.session, executionProfile, tools, mcpTools, disposeRuntime, mcpTools.tools.length > 0 ? "external_mcp_activation_mismatch" : "execution_selection_mismatch", "subset", skills);
|
|
467
|
+
}
|
|
468
|
+
async loadSkills(input) {
|
|
469
|
+
try {
|
|
470
|
+
return await loadSelectedSkills(input.executionProfile.skills ?? [], input.cwd);
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
throw this.failure(input.executionProfile, "skill_load", "skill_load_failed", error instanceof PiSkillError && error.alias !== undefined ? { skill: error.alias } : undefined);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
checkResources(loader, skills, executionProfile, stage) {
|
|
477
|
+
let actual;
|
|
478
|
+
try {
|
|
479
|
+
actual = loader.getSkills().skills;
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
throw this.failure(executionProfile, stage, "skill_load_failed");
|
|
483
|
+
}
|
|
484
|
+
// Compare a multiset so duplicate entries cannot satisfy a selected file twice.
|
|
485
|
+
const identity = (skill) => JSON.stringify([skill.filePath, skill.name]);
|
|
486
|
+
const expected = skills.map(identity).sort();
|
|
487
|
+
const received = actual.map(identity).sort();
|
|
488
|
+
if (expected.length !== received.length || expected.some((entry, index) => entry !== received[index])) {
|
|
489
|
+
throw this.failure(executionProfile, stage, "skill_load_failed");
|
|
490
|
+
}
|
|
491
|
+
if (loader.getPrompts().prompts.length > 0 || loader.getThemes().themes.length > 0) {
|
|
492
|
+
throw this.failure(executionProfile, stage, "pi_session_creation_failed");
|
|
493
|
+
}
|
|
453
494
|
}
|
|
454
495
|
/** Provider, model, authentication and availability, exactly as configured. */
|
|
455
496
|
async resolveVerifiedModel(runtime, executionProfile) {
|
|
@@ -513,8 +554,9 @@ export class SdkPiSessionAdapter {
|
|
|
513
554
|
/** Post-verifies a created session and wraps it; a mismatch disposes it before reporting. */
|
|
514
555
|
async admit(session, executionProfile, tools, mcpTools, disposeSession, toolMismatch,
|
|
515
556
|
// `subset` lets an extension narrow its own active tools; nothing outside the allowlist may be active.
|
|
516
|
-
toolCheck = "exact") {
|
|
557
|
+
toolCheck = "exact", skills = []) {
|
|
517
558
|
try {
|
|
559
|
+
this.checkResources(session.resourceLoader, skills, executionProfile, "postcheck");
|
|
518
560
|
if (session.model?.provider !== executionProfile.provider || session.model.id !== executionProfile.model) {
|
|
519
561
|
throw this.failure(executionProfile, "postcheck", "execution_selection_mismatch");
|
|
520
562
|
}
|
|
@@ -570,6 +612,7 @@ export class SdkPiSessionAdapter {
|
|
|
570
612
|
...(metadata?.toolAlias === undefined ? {} : { toolAlias: metadata.toolAlias }),
|
|
571
613
|
...(metadata?.discoveredToolCount === undefined ? {} : { discoveredToolCount: metadata.discoveredToolCount }),
|
|
572
614
|
...(metadata?.extension === undefined ? {} : { extension: metadata.extension }),
|
|
615
|
+
...(metadata?.skill === undefined ? {} : { skill: metadata.skill }),
|
|
573
616
|
});
|
|
574
617
|
}
|
|
575
618
|
catch {
|
package/dist/server.js
CHANGED
package/dist/setup-cli.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { SetupResult } from "./setup-result.js";
|
|
|
3
3
|
/** Small argv adapter; command execution remains injected in SetupClientAdapter. */
|
|
4
4
|
export interface SetupCliRequest extends SetupRequest {
|
|
5
5
|
readonly json: boolean;
|
|
6
|
+
/** `--customize-skill <name>`: copies a shipped skill into the override directory instead of touching clients. */
|
|
7
|
+
readonly customizeSkill?: string;
|
|
6
8
|
}
|
|
7
9
|
export declare function parseSetupArgs(argv: readonly string[]): SetupCliRequest;
|
|
8
10
|
export declare function runSetupCli(argv: readonly string[], orchestrator: SetupOrchestrator): Promise<SetupResult>;
|
package/dist/setup-cli.js
CHANGED
|
@@ -2,6 +2,7 @@ export function parseSetupArgs(argv) {
|
|
|
2
2
|
let operation;
|
|
3
3
|
let json = false;
|
|
4
4
|
const targets = [];
|
|
5
|
+
let customizeSkill;
|
|
5
6
|
for (let index = 0; index < argv.length; index += 1) {
|
|
6
7
|
const argument = argv[index];
|
|
7
8
|
if (argument === "--json") {
|
|
@@ -24,8 +25,23 @@ export function parseSetupArgs(argv) {
|
|
|
24
25
|
targets.push({ client: parts[0], scope: parts[1], alias: parts[2] });
|
|
25
26
|
continue;
|
|
26
27
|
}
|
|
28
|
+
if (argument === "--customize-skill") {
|
|
29
|
+
if (customizeSkill !== undefined)
|
|
30
|
+
throw new Error("customize_skill_repeated");
|
|
31
|
+
const value = argv[++index];
|
|
32
|
+
if (value === undefined || value === "" || value.startsWith("-"))
|
|
33
|
+
throw new Error("customize_skill_invalid");
|
|
34
|
+
customizeSkill = value;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
27
37
|
throw new Error("argument_unsupported");
|
|
28
38
|
}
|
|
39
|
+
if (customizeSkill !== undefined) {
|
|
40
|
+
// Skill customization is a dry-run or an explicit apply; it never targets a client.
|
|
41
|
+
if (targets.length > 0 || (operation !== undefined && operation !== "dry-run" && operation !== "apply"))
|
|
42
|
+
throw new Error("customize_skill_combination");
|
|
43
|
+
return operation ? { operation, targets, json, customizeSkill } : { targets, json, customizeSkill };
|
|
44
|
+
}
|
|
29
45
|
return operation ? { operation, targets, json } : { targets, json };
|
|
30
46
|
}
|
|
31
47
|
export async function runSetupCli(argv, orchestrator) {
|
package/dist/setup-main.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createProductionSetup } from "./setup.js";
|
|
3
|
-
/** Executable entry point: `node dist/setup-main.js [operation] --target client:scope:alias
|
|
3
|
+
/** Executable entry point: `node dist/setup-main.js [operation] --target client:scope:alias` or `[--apply] --customize-skill <name>`. */
|
|
4
4
|
export interface SetupMainDependencies {
|
|
5
5
|
readonly createSetup?: typeof createProductionSetup;
|
|
6
6
|
readonly packageRoot?: string;
|
package/dist/setup-main.js
CHANGED
|
@@ -5,6 +5,8 @@ import { isEntryPoint } from "./entry-point.js";
|
|
|
5
5
|
import { createProcessRunner } from "./setup-process.js";
|
|
6
6
|
import { currentSetupPlatform } from "./setup-platform.js";
|
|
7
7
|
import { createProductionSetup, isSupportedTarget } from "./setup.js";
|
|
8
|
+
import { customizeSkill } from "./skill-customize.js";
|
|
9
|
+
import { SKILL_NAME_PATTERN } from "./skill-library.js";
|
|
8
10
|
import { formatSetupHuman, formatSetupResult, parseSetupArgs } from "./setup-cli.js";
|
|
9
11
|
import { result } from "./setup-result.js";
|
|
10
12
|
export async function main(argv = process.argv.slice(2), dependencies = {}) {
|
|
@@ -18,11 +20,21 @@ export async function main(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
18
20
|
return write(cliFailure("dry-run", [], "usage_invalid", 64), json);
|
|
19
21
|
}
|
|
20
22
|
const operation = request.operation ?? "dry-run";
|
|
23
|
+
const failureTargets = request.customizeSkill === undefined ? request.targets : [skillTarget(request.customizeSkill)];
|
|
21
24
|
if (!currentSetupPlatform())
|
|
22
|
-
return write(cliFailure(operation,
|
|
25
|
+
return write(cliFailure(operation, failureTargets, "platform_unsupported", 1), request.json);
|
|
23
26
|
const config = process.env.PI_SESSION_MCP_CONFIG;
|
|
24
27
|
if (!config)
|
|
25
|
-
return write(cliFailure(operation,
|
|
28
|
+
return write(cliFailure(operation, failureTargets, "config_required", 1), request.json);
|
|
29
|
+
if (request.customizeSkill !== undefined) {
|
|
30
|
+
const name = request.customizeSkill;
|
|
31
|
+
try {
|
|
32
|
+
return write(await customizeSkill({ name, configPath: resolve(config), apply: operation === "apply", ...(dependencies.packageRoot === undefined ? {} : { packageRoot: dependencies.packageRoot }) }), request.json);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return write(cliFailure(operation, [skillTarget(name)], "skill_customize_failed", 1), request.json);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
26
38
|
const root = dependencies.packageRoot ?? resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
27
39
|
const controller = new AbortController();
|
|
28
40
|
let interrupted;
|
|
@@ -51,8 +63,11 @@ export async function main(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
51
63
|
process.removeListener("SIGTERM", onTerminate);
|
|
52
64
|
}
|
|
53
65
|
}
|
|
66
|
+
/** The skill name is echoed only when it is a valid name; arbitrary input never reaches a report. */
|
|
67
|
+
function skillTarget(name) { return { client: "skills", scope: "override", alias: SKILL_NAME_PATTERN.test(name) ? name : "invalid" }; }
|
|
68
|
+
function isReportableTarget(target) { return isSupportedTarget(target) || (target.client === "skills" && target.scope === "override" && (target.alias === "invalid" || SKILL_NAME_PATTERN.test(target.alias))); }
|
|
54
69
|
function cliFailure(operation, targets, code, exitCode) {
|
|
55
|
-
const safeTargets = targets.filter(
|
|
70
|
+
const safeTargets = targets.filter(isReportableTarget);
|
|
56
71
|
const reported = safeTargets.length === targets.length && safeTargets.length > 0 ? safeTargets : [{ client: "invalid", scope: "invalid", alias: "invalid" }];
|
|
57
72
|
return result(operation, reported.map((target) => ({ target, status: "failed", code })), "failed", exitCode);
|
|
58
73
|
}
|
|
@@ -6,6 +6,11 @@ export interface OwnershipRecord {
|
|
|
6
6
|
readonly previous: "absent" | "equivalent";
|
|
7
7
|
readonly phase: "pending" | "owned";
|
|
8
8
|
readonly transactionId: string;
|
|
9
|
+
/**
|
|
10
|
+
* Skill records only, while pending: the fingerprint of setup's own copy that the pending
|
|
11
|
+
* update replaces, so an interrupted update can still be completed or removed.
|
|
12
|
+
*/
|
|
13
|
+
readonly replaces?: string;
|
|
9
14
|
}
|
|
10
15
|
export interface RegistrationSnapshot {
|
|
11
16
|
/** Opaque adapter-owned snapshot; implementations must not persist secrets. */
|
|
@@ -51,3 +56,4 @@ export declare class DurableOwnershipStore implements OwnershipStore {
|
|
|
51
56
|
private withLock;
|
|
52
57
|
private write;
|
|
53
58
|
}
|
|
59
|
+
export declare function isRecord(value: unknown): value is OwnershipRecord;
|
package/dist/setup-ownership.js
CHANGED
|
@@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
5
|
import { acquireSetupGuardian } from "./setup-process.js";
|
|
6
6
|
import { assertSupportedFilesystem, currentSetupPlatform, openLockedDarwin } from "./setup-platform.js";
|
|
7
|
+
import { SKILL_NAME_PATTERN } from "./skill-library.js";
|
|
7
8
|
/** Safe default for one process; callers needing recovery inject a durable store. */
|
|
8
9
|
export class MemoryOwnershipStore {
|
|
9
10
|
entries = new Map();
|
|
@@ -209,10 +210,33 @@ async function syncDirectory(path) {
|
|
|
209
210
|
await handle.close();
|
|
210
211
|
}
|
|
211
212
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
213
|
+
/** `skill:<name>` for a home slot; `skill:<name>@<16 hex>` for a project slot bound to an opaque project identity. */
|
|
214
|
+
const SKILL_RECORD_ALIAS = /^skill:([^@]+)(?:@([0-9a-f]{16}))?$/;
|
|
215
|
+
export function isRecord(value) {
|
|
216
|
+
if (!value || typeof value !== "object")
|
|
217
|
+
return false;
|
|
218
|
+
const x = value;
|
|
219
|
+
const keys = ["target", "fingerprint", "previous", "phase", "transactionId"];
|
|
220
|
+
if (!exactKeys(x, keys) && !exactKeys(x, [...keys, "replaces"]))
|
|
221
|
+
return false;
|
|
222
|
+
if (!x.target || typeof x.target !== "object")
|
|
223
|
+
return false;
|
|
224
|
+
const target = x.target;
|
|
225
|
+
if (!exactKeys(target, ["client", "scope", "alias"]))
|
|
226
|
+
return false;
|
|
227
|
+
const { client, scope, alias } = target;
|
|
228
|
+
const registration = alias === "pi-session-mcp" &&
|
|
229
|
+
(client === "codex" ? scope === "user" : client === "claude-code" && typeof scope === "string" && ["user", "project", "local"].includes(scope));
|
|
230
|
+
const skillAlias = typeof alias === "string" ? SKILL_RECORD_ALIAS.exec(alias) : null;
|
|
231
|
+
const skill = skillAlias !== null && SKILL_NAME_PATTERN.test(skillAlias[1]) &&
|
|
232
|
+
(client === "codex" ? scope === "user" && skillAlias[2] === undefined
|
|
233
|
+
: client === "claude-code" && ((scope === "user" && skillAlias[2] === undefined) || (scope === "project" && skillAlias[2] !== undefined)));
|
|
234
|
+
// `replaces` belongs only to a pending skill record.
|
|
235
|
+
if ("replaces" in x && !(skill && x.phase === "pending" && typeof x.replaces === "string" && /^[a-f0-9]{64}$/.test(x.replaces)))
|
|
236
|
+
return false;
|
|
237
|
+
return (registration || skill) && typeof x.fingerprint === "string" && /^[a-f0-9]{64}$/.test(x.fingerprint) &&
|
|
238
|
+
(x.phase === "pending" || x.phase === "owned") && x.previous === "absent" &&
|
|
239
|
+
typeof x.transactionId === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(x.transactionId);
|
|
240
|
+
}
|
|
217
241
|
function exactKeys(value, expected) { const keys = Object.keys(value).sort(); return keys.length === expected.length && [...expected].sort().every((key, index) => keys[index] === key); }
|
|
218
242
|
const key = (target) => `${target.client}\u0000${target.scope}\u0000${target.alias}`;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { OwnershipAccess } from "./setup-ownership.js";
|
|
2
|
+
import type { SetupFinding, SetupOperation, SetupTarget } from "./setup-result.js";
|
|
3
|
+
export interface SetupSkillInstaller {
|
|
4
|
+
/** Called under the registration's ownership transaction for every mutation. */
|
|
5
|
+
run(operation: SetupOperation, target: SetupTarget, ownership: OwnershipAccess): Promise<readonly SetupFinding[]>;
|
|
6
|
+
/**
|
|
7
|
+
* Whether setup still owns an installed skill of exactly this registration target, so that a
|
|
8
|
+
* remove may finish the skill cleanup after the registration is already gone.
|
|
9
|
+
*/
|
|
10
|
+
owns(target: SetupTarget, ownership: OwnershipAccess): Promise<boolean>;
|
|
11
|
+
}
|
|
12
|
+
export interface SetupSkillRoots {
|
|
13
|
+
readonly packageRoot: string;
|
|
14
|
+
readonly configPath: string;
|
|
15
|
+
readonly home: string;
|
|
16
|
+
readonly cwd: string;
|
|
17
|
+
}
|
|
18
|
+
/** Paths remain internal. Ownership identifies a client slot, never its location. */
|
|
19
|
+
export declare class FileSetupSkillInstaller implements SetupSkillInstaller {
|
|
20
|
+
private readonly roots;
|
|
21
|
+
constructor(roots: SetupSkillRoots);
|
|
22
|
+
run(operation: SetupOperation, target: SetupTarget, ownership: OwnershipAccess): Promise<readonly SetupFinding[]>;
|
|
23
|
+
owns(target: SetupTarget, ownership: OwnershipAccess): Promise<boolean>;
|
|
24
|
+
/**
|
|
25
|
+
* The slot path and its ownership key. A project may be untrusted: only its root is
|
|
26
|
+
* canonicalized, and a link below it (for example `.claude` pointing elsewhere) makes the
|
|
27
|
+
* slot divergent, so setup never writes outside the project. In the operator's home the
|
|
28
|
+
* client's skills directory may be the operator's own link, for example into a dotfiles
|
|
29
|
+
* checkout, so it is followed. The skill entry itself is never followed in either place.
|
|
30
|
+
* A project slot's ownership is bound to an opaque, path-free identity of the project, so a
|
|
31
|
+
* remove in another project never touches this project's copy.
|
|
32
|
+
*/
|
|
33
|
+
private location;
|
|
34
|
+
private library;
|
|
35
|
+
private skill;
|
|
36
|
+
private replace;
|
|
37
|
+
}
|