pi-session-mcp 0.0.0-stage → 0.6.2

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.
Files changed (88) hide show
  1. package/.pi-session-mcp-cli/config.js +88 -0
  2. package/.pi-session-mcp-cli/doctor-cli.js +11 -0
  3. package/.pi-session-mcp-cli/doctor.js +178 -0
  4. package/.pi-session-mcp-cli/execution-profile.js +29 -0
  5. package/.pi-session-mcp-cli/mcp-config.js +70 -0
  6. package/.pi-session-mcp-cli/package-metadata.js +32 -0
  7. package/CHANGELOG.md +62 -0
  8. package/CONTRIBUTING.md +57 -0
  9. package/LICENSE +21 -0
  10. package/README.md +183 -2
  11. package/SECURITY.md +37 -0
  12. package/dist/capabilities.d.ts +25 -0
  13. package/dist/capabilities.js +26 -0
  14. package/dist/client-adapters/claude-code.d.ts +4 -0
  15. package/dist/client-adapters/claude-code.js +33 -0
  16. package/dist/client-adapters/codex.d.ts +4 -0
  17. package/dist/client-adapters/codex.js +73 -0
  18. package/dist/client-adapters/command.d.ts +12 -0
  19. package/dist/client-adapters/command.js +25 -0
  20. package/dist/client-adapters/index.d.ts +4 -0
  21. package/dist/client-adapters/index.js +3 -0
  22. package/dist/client-adapters/mcp-verifier.d.ts +20 -0
  23. package/dist/client-adapters/mcp-verifier.js +174 -0
  24. package/dist/client-adapters/types.d.ts +54 -0
  25. package/dist/client-adapters/types.js +1 -0
  26. package/dist/config.d.ts +50 -0
  27. package/dist/config.js +88 -0
  28. package/dist/doctor-cli.d.ts +1 -0
  29. package/dist/doctor-cli.js +11 -0
  30. package/dist/doctor.d.ts +47 -0
  31. package/dist/doctor.js +178 -0
  32. package/dist/entry-point.d.ts +5 -0
  33. package/dist/entry-point.js +16 -0
  34. package/dist/execution-profile.d.ts +37 -0
  35. package/dist/execution-profile.js +29 -0
  36. package/dist/main.d.ts +2 -0
  37. package/dist/main.js +51 -0
  38. package/dist/mcp-config.d.ts +22 -0
  39. package/dist/mcp-config.js +70 -0
  40. package/dist/mcp-session-tools.d.ts +54 -0
  41. package/dist/mcp-session-tools.js +323 -0
  42. package/dist/mcp-stdio-client.d.ts +3 -0
  43. package/dist/mcp-stdio-client.js +59 -0
  44. package/dist/package-metadata.d.ts +18 -0
  45. package/dist/package-metadata.js +32 -0
  46. package/dist/pi-adapter.d.ts +42 -0
  47. package/dist/pi-adapter.js +22 -0
  48. package/dist/process-diagnostics.d.ts +8 -0
  49. package/dist/process-diagnostics.js +9 -0
  50. package/dist/sdk-pi-adapter.d.ts +75 -0
  51. package/dist/sdk-pi-adapter.js +410 -0
  52. package/dist/server.d.ts +5 -0
  53. package/dist/server.js +102 -0
  54. package/dist/session-registry.d.ts +104 -0
  55. package/dist/session-registry.js +373 -0
  56. package/dist/setup-cli.d.ts +11 -0
  57. package/dist/setup-cli.js +38 -0
  58. package/dist/setup-command-guardian.d.ts +2 -0
  59. package/dist/setup-command-guardian.js +297 -0
  60. package/dist/setup-main.d.ts +8 -0
  61. package/dist/setup-main.js +60 -0
  62. package/dist/setup-ownership.d.ts +53 -0
  63. package/dist/setup-ownership.js +228 -0
  64. package/dist/setup-process.d.ts +30 -0
  65. package/dist/setup-process.js +385 -0
  66. package/dist/setup-result.d.ts +22 -0
  67. package/dist/setup-result.js +8 -0
  68. package/dist/setup.d.ts +55 -0
  69. package/dist/setup.js +327 -0
  70. package/dist/shutdown.d.ts +10 -0
  71. package/dist/shutdown.js +52 -0
  72. package/docs/acceptance.md +227 -0
  73. package/docs/adr/0001-stdio-first.md +12 -0
  74. package/docs/adr/0002-embedded-pi-sdk.md +11 -0
  75. package/docs/adr/0003-post-mvp-maintenance.md +60 -0
  76. package/docs/adr/0004-client-contract-not-version.md +31 -0
  77. package/docs/architecture.md +82 -0
  78. package/docs/client-setup-design.md +90 -0
  79. package/docs/client-setup.md +207 -0
  80. package/docs/doctor-design.md +47 -0
  81. package/docs/installation.md +352 -0
  82. package/docs/releasing.md +216 -0
  83. package/docs/session-mcp.md +260 -0
  84. package/docs/threat-model.md +89 -0
  85. package/docs/tool-contracts.md +143 -0
  86. package/package.json +64 -4
  87. package/pi-session-mcp.example.json +21 -0
  88. package/scripts/doctor.mjs +72 -0
@@ -0,0 +1,54 @@
1
+ /** Safe, client-neutral contract used by the setup lifecycle. */
2
+ export type ClientName = "codex" | "claude-code";
3
+ export type ClientScope = "user" | "project" | "local";
4
+ export type RegistrationState = "absent" | "equivalent" | "divergent" | "unsupported";
5
+ export interface CommandResult {
6
+ readonly exitCode: number;
7
+ readonly stdout: string;
8
+ readonly stderr: string;
9
+ }
10
+ export interface CommandRunner {
11
+ run(command: string, args: readonly string[], signal?: AbortSignal): Promise<CommandResult>;
12
+ }
13
+ export interface CommandExecution {
14
+ readonly signal?: AbortSignal;
15
+ readonly runner?: CommandRunner;
16
+ }
17
+ export interface ClientRegistration {
18
+ readonly client: ClientName;
19
+ readonly scope: ClientScope;
20
+ readonly name: "pi-session-mcp";
21
+ readonly state: RegistrationState;
22
+ readonly command: "node" | "unknown";
23
+ readonly hasExpectedConfig: boolean;
24
+ readonly supported: boolean;
25
+ }
26
+ export interface ClientCapabilities {
27
+ readonly client: ClientName;
28
+ readonly version: string;
29
+ readonly supportsJson: boolean;
30
+ readonly supportsAdd: boolean;
31
+ readonly supportsRemove: boolean;
32
+ readonly scopes: readonly ClientScope[];
33
+ }
34
+ export interface RegistrationIntent {
35
+ readonly scope: ClientScope;
36
+ readonly nodePath: string;
37
+ readonly entryPath: string;
38
+ readonly configPath: string;
39
+ }
40
+ export interface RegistrationPlan {
41
+ readonly client: ClientName;
42
+ readonly scope: ClientScope;
43
+ readonly state: RegistrationState;
44
+ readonly operation: "none" | "add" | "replace";
45
+ readonly reason: "already_registered" | "missing" | "divergent" | "unsupported";
46
+ }
47
+ export interface ClientAdapter {
48
+ readonly client: ClientName;
49
+ discover(): Promise<ClientCapabilities>;
50
+ inspect(scope: ClientScope, intent: RegistrationIntent): Promise<ClientRegistration>;
51
+ plan(intent: RegistrationIntent): Promise<RegistrationPlan>;
52
+ add(intent: RegistrationIntent, execution?: CommandExecution): Promise<void>;
53
+ remove(scope: ClientScope, execution?: CommandExecution): Promise<void>;
54
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+ import { type ConfiguredExecutionProfile } from "./execution-profile.js";
3
+ export type ConfigErrorCode = "config_path_required" | "invalid_execution_profile_config" | "invalid_thinking_level";
4
+ export declare class ConfigError extends Error {
5
+ readonly code: ConfigErrorCode;
6
+ constructor(code: ConfigErrorCode);
7
+ }
8
+ export declare const configSchema: z.ZodObject<{
9
+ workspaces: z.ZodRecord<z.ZodString, z.ZodString>;
10
+ executionProfiles: z.ZodRecord<z.ZodString, z.ZodObject<{
11
+ default: z.ZodBoolean;
12
+ permissionProfile: z.ZodEnum<{
13
+ coding: "coding";
14
+ "read-only": "read-only";
15
+ }>;
16
+ provider: z.ZodString;
17
+ model: z.ZodString;
18
+ thinkingLevel: z.ZodEnum<{
19
+ high: "high";
20
+ low: "low";
21
+ max: "max";
22
+ medium: "medium";
23
+ minimal: "minimal";
24
+ off: "off";
25
+ xhigh: "xhigh";
26
+ }>;
27
+ mcpServers: z.ZodOptional<z.ZodCustom<Readonly<Record<string, import("./mcp-config.js").McpServerConfig>>, Readonly<Record<string, import("./mcp-config.js").McpServerConfig>>>>;
28
+ }, z.core.$strict>>;
29
+ }, z.core.$strict>;
30
+ /** The only supported reload behavior: a running controller keeps the snapshot it loaded at startup. */
31
+ export declare const RELOAD_POLICY: "restart-required";
32
+ export type ReloadPolicy = typeof RELOAD_POLICY;
33
+ export declare const CONFIGURATION_FINGERPRINT_PATTERN: RegExp;
34
+ /** Path-free, secret-free metadata captured from the exact configuration bytes read at load time. */
35
+ export interface ConfigurationMetadata {
36
+ readonly fingerprint: `sha256:${string}`;
37
+ readonly reloadPolicy: ReloadPolicy;
38
+ }
39
+ export interface AppConfig {
40
+ readonly workspaces: ReadonlyMap<string, string>;
41
+ readonly executionProfiles: ReadonlyMap<string, ConfiguredExecutionProfile>;
42
+ readonly defaultExecutionProfile: string;
43
+ readonly configuration: ConfigurationMetadata;
44
+ }
45
+ /**
46
+ * The configuration path comes only from the caller or from `PI_SESSION_MCP_CONFIG`.
47
+ * There is no default file: a server's working directory is the client's project,
48
+ * which may be untrusted, so a file found there must never become the policy.
49
+ */
50
+ export declare function loadConfig(path?: string | undefined): Promise<AppConfig>;
package/dist/config.js ADDED
@@ -0,0 +1,88 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { dirname, resolve } from "node:path";
4
+ import { z } from "zod";
5
+ import { isMcpServers, snapshotMcpServers } from "./mcp-config.js";
6
+ import { ALIAS_PATTERN, ImmutableMap, } from "./execution-profile.js";
7
+ const alias = z.string().regex(ALIAS_PATTERN);
8
+ const permissionProfile = z.enum(["read-only", "coding"]);
9
+ const thinkingLevel = z.enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
10
+ const profileSchema = z.object({
11
+ default: z.boolean(), permissionProfile, provider: z.string().trim().min(1).max(256),
12
+ model: z.string().trim().min(1).max(256), thinkingLevel,
13
+ mcpServers: z.custom((value) => isMcpServers(value)).optional(),
14
+ }).strict().superRefine((profile, context) => {
15
+ if (profile.mcpServers !== undefined && !isMcpServers(profile.mcpServers, profile.permissionProfile)) {
16
+ context.addIssue({ code: "custom", path: ["mcpServers"], message: "MCP tool grants do not match the permission profile" });
17
+ }
18
+ });
19
+ const CONFIG_ERROR_MESSAGES = {
20
+ config_path_required: "Configuration path is required",
21
+ invalid_execution_profile_config: "Execution profile configuration is invalid",
22
+ invalid_thinking_level: "Execution profile thinking level is invalid",
23
+ };
24
+ export class ConfigError extends Error {
25
+ code;
26
+ constructor(code) {
27
+ super(CONFIG_ERROR_MESSAGES[code]);
28
+ this.name = "ConfigError";
29
+ this.code = code;
30
+ }
31
+ }
32
+ export const configSchema = z.object({
33
+ workspaces: z.record(alias, z.string().min(1)),
34
+ executionProfiles: z.record(alias, profileSchema).superRefine((profiles, context) => {
35
+ const entries = Object.entries(profiles);
36
+ if (entries.length === 0)
37
+ context.addIssue({ code: "custom", message: "At least one execution profile is required" });
38
+ const defaults = entries.filter(([, profile]) => profile.default);
39
+ if (defaults.length !== 1)
40
+ context.addIssue({ code: "custom", message: "Exactly one execution profile must be the default" });
41
+ if (defaults.length === 1 && defaults[0][1].permissionProfile !== "read-only")
42
+ context.addIssue({ code: "custom", message: "The default execution profile must be read-only" });
43
+ }),
44
+ }).strict();
45
+ /** The only supported reload behavior: a running controller keeps the snapshot it loaded at startup. */
46
+ export const RELOAD_POLICY = "restart-required";
47
+ export const CONFIGURATION_FINGERPRINT_PATTERN = /^sha256:[0-9a-f]{64}$/;
48
+ function configurationFingerprint(bytes) {
49
+ return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
50
+ }
51
+ /**
52
+ * The configuration path comes only from the caller or from `PI_SESSION_MCP_CONFIG`.
53
+ * There is no default file: a server's working directory is the client's project,
54
+ * which may be untrusted, so a file found there must never become the policy.
55
+ */
56
+ export async function loadConfig(path = process.env.PI_SESSION_MCP_CONFIG) {
57
+ if (!path)
58
+ throw new ConfigError("config_path_required");
59
+ const absoluteConfig = resolve(path);
60
+ // One read: the same bytes feed both the fingerprint and the parse.
61
+ const bytes = await readFile(absoluteConfig);
62
+ const configuration = Object.freeze({
63
+ fingerprint: configurationFingerprint(bytes),
64
+ reloadPolicy: RELOAD_POLICY,
65
+ });
66
+ const input = JSON.parse(bytes.toString("utf8"));
67
+ let parsed;
68
+ try {
69
+ parsed = configSchema.parse(input);
70
+ }
71
+ catch (error) {
72
+ if (error instanceof z.ZodError) {
73
+ const thinkingLevelIssue = error.issues.some((issue) => issue.path.at(-1) === "thinkingLevel");
74
+ throw new ConfigError(thinkingLevelIssue ? "invalid_thinking_level" : "invalid_execution_profile_config");
75
+ }
76
+ throw error;
77
+ }
78
+ const base = dirname(absoluteConfig);
79
+ const workspaces = new ImmutableMap(Object.entries(parsed.workspaces).map(([key, value]) => [key, resolve(base, value)]));
80
+ const executionProfiles = new ImmutableMap(Object.entries(parsed.executionProfiles).map(([key, { mcpServers, ...value }]) => [key, Object.freeze({
81
+ alias: key, ...value,
82
+ ...(mcpServers === undefined ? {} : { mcpServers: snapshotMcpServers(mcpServers) }),
83
+ })]));
84
+ const defaultEntry = [...executionProfiles].find(([, profile]) => profile.default);
85
+ if (!defaultEntry)
86
+ throw new Error("Execution profile configuration is invalid");
87
+ return { workspaces, executionProfiles, defaultExecutionProfile: defaultEntry[0], configuration };
88
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import { parseDoctorArgs, renderDoctorHuman, renderDoctorJson, runDoctor } from "./doctor.js";
2
+ const options = parseDoctorArgs(process.argv.slice(2));
3
+ if (!options) {
4
+ process.stderr.write("Usage: doctor [--json]\n");
5
+ process.exitCode = 64;
6
+ }
7
+ else {
8
+ const result = await runDoctor();
9
+ process.stdout.write(options.json ? renderDoctorJson(result) : renderDoctorHuman(result));
10
+ process.exitCode = result.exitCode;
11
+ }
@@ -0,0 +1,47 @@
1
+ import { type AppConfig } from "./config.js";
2
+ export type DoctorSeverity = "ok" | "warning" | "error";
3
+ export type DoctorExitCode = 0 | 1 | 2 | 64;
4
+ 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";
5
+ export interface DoctorCheck {
6
+ readonly id: DoctorCheckId;
7
+ readonly severity: DoctorSeverity;
8
+ readonly subject: string;
9
+ readonly remediation?: string;
10
+ }
11
+ export interface DoctorResult {
12
+ readonly schemaVersion: 1;
13
+ readonly ok: boolean;
14
+ readonly packageVersion?: string;
15
+ readonly checks: readonly DoctorCheck[];
16
+ readonly exitCode: Exclude<DoctorExitCode, 64>;
17
+ }
18
+ export interface DoctorPackageMetadata {
19
+ readonly version: string;
20
+ readonly engines?: {
21
+ readonly node?: string;
22
+ };
23
+ readonly lockVersion?: string;
24
+ readonly expectedVersion?: string;
25
+ }
26
+ export interface DoctorFileSystem {
27
+ readonly readFile: (path: string, encoding: "utf8") => Promise<string>;
28
+ readonly stat: (path: string) => Promise<{
29
+ readonly isDirectory: () => boolean;
30
+ readonly isFile: () => boolean;
31
+ readonly mtimeMs?: number;
32
+ }>;
33
+ readonly access: (path: string, mode: number) => Promise<void>;
34
+ }
35
+ export interface DoctorDependencies {
36
+ readonly packageRoot?: string;
37
+ readonly nodeVersion?: string;
38
+ readonly fileSystem?: DoctorFileSystem;
39
+ readonly loadConfig?: () => Promise<AppConfig>;
40
+ readonly packageMetadata?: () => Promise<DoctorPackageMetadata>;
41
+ }
42
+ export declare function runDoctor(dependencies?: DoctorDependencies): Promise<DoctorResult>;
43
+ export declare function renderDoctorJson(result: DoctorResult): string;
44
+ export declare function renderDoctorHuman(result: DoctorResult): string;
45
+ export declare function parseDoctorArgs(args: readonly string[]): {
46
+ json: boolean;
47
+ } | undefined;
package/dist/doctor.js ADDED
@@ -0,0 +1,178 @@
1
+ import { access, readFile, readdir, stat } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { loadConfig } from "./config.js";
5
+ import { readPackageMetadata } from "./package-metadata.js";
6
+ const defaultFileSystem = { readFile, stat, access };
7
+ 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"];
8
+ function failure(id, subject, remediation) { return Object.freeze({ id, severity: "error", subject, remediation }); }
9
+ function warning(id, subject, remediation) { return Object.freeze({ id, severity: "warning", subject, remediation }); }
10
+ function ok(id, subject) { return Object.freeze({ id, severity: "ok", subject }); }
11
+ function isMissing(error) { return typeof error === "object" && error !== null && "code" in error && ["ENOENT", "config_path_required"].includes(String(error.code)); }
12
+ /** The lockfile's modification time, or undefined for an npm installation, which has no lockfile. */
13
+ async function lockfileMtime(fs, root) {
14
+ try {
15
+ return (await fs.stat(join(root, "package-lock.json"))).mtimeMs;
16
+ }
17
+ catch (error) {
18
+ if (isMissing(error))
19
+ return undefined;
20
+ throw error;
21
+ }
22
+ }
23
+ function isUnreadable(error) { return typeof error === "object" && error !== null && "code" in error && ["EACCES", "EPERM"].includes(String(error.code)); }
24
+ function isSemver(value) { return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value); }
25
+ function satisfiesNode(version, range) {
26
+ const minimum = range.match(/>=\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
27
+ const actual = version.match(/^(\d+)\.(\d+)\.(\d+)/);
28
+ if (!minimum || !actual)
29
+ return false;
30
+ const want = [Number(minimum[1]), Number(minimum[2] ?? 0), Number(minimum[3] ?? 0)];
31
+ const got = [Number(actual[1]), Number(actual[2]), Number(actual[3])];
32
+ return got[0] > want[0] || (got[0] === want[0] && (got[1] > want[1] || (got[1] === want[1] && got[2] >= want[2])));
33
+ }
34
+ /** Build inputs beyond the manifests. An installed package has none of them, which is not staleness. */
35
+ async function productionBuildInputMtimes(root) {
36
+ const mtimes = [];
37
+ for (const name of ["tsconfig.json", "tsconfig.build.json"]) {
38
+ try {
39
+ mtimes.push((await stat(join(root, name))).mtimeMs);
40
+ }
41
+ catch (error) {
42
+ if (!isMissing(error))
43
+ throw error;
44
+ }
45
+ }
46
+ const visit = async (directory) => {
47
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
48
+ const path = join(directory, entry.name);
49
+ if (entry.isDirectory())
50
+ await visit(path);
51
+ else if (entry.isFile() && entry.name.endsWith(".ts"))
52
+ mtimes.push((await stat(path)).mtimeMs);
53
+ }
54
+ };
55
+ try {
56
+ await visit(join(root, "src"));
57
+ }
58
+ catch (error) {
59
+ if (!isMissing(error))
60
+ throw error;
61
+ }
62
+ return mtimes;
63
+ }
64
+ export async function runDoctor(dependencies = {}) {
65
+ const fs = dependencies.fileSystem ?? defaultFileSystem;
66
+ const root = dependencies.packageRoot ?? process.cwd();
67
+ const checks = [];
68
+ let metadata;
69
+ try {
70
+ const injected = dependencies.packageMetadata !== undefined;
71
+ if (dependencies.packageMetadata)
72
+ metadata = await dependencies.packageMetadata();
73
+ else {
74
+ metadata = await readPackageMetadata(root, fs.readFile);
75
+ }
76
+ if (!metadata || !isSemver(metadata.version) || typeof metadata.engines?.node !== "string" || !/>=\s*\d+/.test(metadata.engines.node))
77
+ throw new Error("invalid package metadata");
78
+ if (!injected && metadata.lockVersion !== undefined && !isSemver(metadata.lockVersion))
79
+ throw new Error("invalid lock metadata");
80
+ }
81
+ catch {
82
+ checks.push(failure("package_metadata_unreadable", "package metadata", "repair package metadata"));
83
+ }
84
+ const nodeRange = metadata?.engines?.node;
85
+ if (nodeRange && !satisfiesNode(dependencies.nodeVersion ?? process.versions.node, nodeRange))
86
+ checks.push(failure("node_version_unsupported", "Node.js", "use a supported Node.js version"));
87
+ else if (nodeRange)
88
+ checks.push(ok("node_version_unsupported", "Node.js"));
89
+ let buildMtime;
90
+ try {
91
+ const build = await fs.stat(join(root, "dist", "main.js"));
92
+ if (build.isFile()) {
93
+ buildMtime = build.mtimeMs;
94
+ checks.push(ok("build_missing", "build output"));
95
+ }
96
+ else
97
+ checks.push(failure("build_missing", "build output", "run the production build"));
98
+ }
99
+ catch (error) {
100
+ checks.push(failure("build_missing", "build output", isMissing(error) ? "run the production build" : "check build output access"));
101
+ }
102
+ if (buildMtime !== undefined) {
103
+ try {
104
+ const packageMtime = (await fs.stat(join(root, "package.json"))).mtimeMs;
105
+ const lockMtime = await lockfileMtime(fs, root);
106
+ const inputMtimes = [packageMtime, lockMtime].filter((value) => value !== undefined);
107
+ if (!dependencies.fileSystem)
108
+ inputMtimes.push(...await productionBuildInputMtimes(root));
109
+ if (inputMtimes.some((mtime) => mtime > buildMtime))
110
+ checks.push(warning("build_stale", "build output", "run the production build"));
111
+ else
112
+ checks.push(ok("build_stale", "build output"));
113
+ }
114
+ catch {
115
+ checks.push(warning("build_stale", "build output", "rebuild to establish current build state"));
116
+ }
117
+ }
118
+ if (metadata) {
119
+ const expectedVersion = metadata.expectedVersion ?? metadata.lockVersion;
120
+ if (expectedVersion && metadata.version !== expectedVersion)
121
+ checks.push(warning("version_mismatch", "package version", "reinstall matching build artifacts"));
122
+ else
123
+ checks.push(ok("version_mismatch", "package version"));
124
+ }
125
+ let config;
126
+ try {
127
+ config = await (dependencies.loadConfig ?? (() => loadConfig()))();
128
+ checks.push(ok("config_invalid", "configuration"));
129
+ }
130
+ catch (error) {
131
+ if (isMissing(error))
132
+ checks.push(failure("config_missing", "configuration", "create PI_SESSION_MCP_CONFIG configuration"));
133
+ else if (isUnreadable(error))
134
+ checks.push(failure("config_unreadable", "configuration", "grant configuration read access"));
135
+ else
136
+ checks.push(failure("config_invalid", "configuration", "fix configuration using the documented schema"));
137
+ }
138
+ if (config) {
139
+ for (const [alias, path] of [...config.workspaces].sort(([a], [b]) => a.localeCompare(b))) {
140
+ try {
141
+ const info = await fs.stat(path);
142
+ if (!info.isDirectory())
143
+ checks.push(failure("workspace_not_directory", alias, "point the workspace alias to a directory"));
144
+ else {
145
+ checks.push(ok("workspace_not_directory", alias));
146
+ try {
147
+ await fs.access(path, constants.R_OK);
148
+ checks.push(ok("workspace_unreadable", alias));
149
+ }
150
+ catch {
151
+ checks.push(failure("workspace_unreadable", alias, "grant workspace read access"));
152
+ }
153
+ const canWrite = [...config.executionProfiles.values()].some((profile) => profile.permissionProfile === "coding");
154
+ if (canWrite) {
155
+ try {
156
+ await fs.access(path, constants.W_OK);
157
+ checks.push(ok("workspace_not_writable", alias));
158
+ }
159
+ catch {
160
+ checks.push(failure("workspace_not_writable", alias, "grant coding workspace write access"));
161
+ }
162
+ }
163
+ }
164
+ }
165
+ catch (error) {
166
+ checks.push(failure(isMissing(error) ? "workspace_missing" : "workspace_unreadable", alias, isMissing(error) ? "fix the workspace alias target" : "grant workspace access"));
167
+ }
168
+ }
169
+ }
170
+ const rank = new Map(CHECK_ORDER.map((id, index) => [id, index]));
171
+ checks.sort((a, b) => (rank.get(a.id) ?? 999) - (rank.get(b.id) ?? 999) || a.subject.localeCompare(b.subject));
172
+ const hasError = checks.some((check) => check.severity === "error");
173
+ const result = { schemaVersion: 1, ok: !hasError, ...(metadata ? { packageVersion: metadata.version } : {}), checks: Object.freeze(checks), exitCode: hasError ? 2 : checks.some((check) => check.severity === "warning") ? 1 : 0 };
174
+ return Object.freeze(result);
175
+ }
176
+ export function renderDoctorJson(result) { return JSON.stringify(result) + "\n"; }
177
+ export function renderDoctorHuman(result) { return result.checks.map((check) => `${check.severity.toUpperCase()} ${check.id} [${check.subject}]${check.remediation ? `: ${check.remediation}` : ""}`).join("\n") + "\n"; }
178
+ export function parseDoctorArgs(args) { return args.length === 0 ? { json: false } : args.length === 1 && args[0] === "--json" ? { json: true } : undefined; }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Whether the module at `importMetaUrl` is the script that Node started. npm
3
+ * installs `bin` entries as symlinks, so the comparison uses real paths.
4
+ */
5
+ export declare function isEntryPoint(importMetaUrl: string, argv1: string | undefined): boolean;
@@ -0,0 +1,16 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ /**
4
+ * Whether the module at `importMetaUrl` is the script that Node started. npm
5
+ * installs `bin` entries as symlinks, so the comparison uses real paths.
6
+ */
7
+ export function isEntryPoint(importMetaUrl, argv1) {
8
+ if (!argv1)
9
+ return false;
10
+ try {
11
+ return realpathSync(fileURLToPath(importMetaUrl)) === realpathSync(argv1);
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
@@ -0,0 +1,37 @@
1
+ import { type McpServers } from "./mcp-config.js";
2
+ export declare const ALIAS_PATTERN: RegExp;
3
+ export type PermissionProfile = "read-only" | "coding";
4
+ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
5
+ /** A profile as it appears in the strictly validated application config. */
6
+ export interface ConfiguredExecutionProfile {
7
+ readonly alias: string;
8
+ readonly default: boolean;
9
+ readonly permissionProfile: PermissionProfile;
10
+ readonly provider: string;
11
+ readonly model: string;
12
+ readonly thinkingLevel: ThinkingLevel;
13
+ readonly mcpServers?: McpServers;
14
+ }
15
+ /** The immutable, server-side selection passed to the Pi adapter. */
16
+ export interface ResolvedExecutionProfile {
17
+ readonly alias: string;
18
+ readonly permissionProfile: PermissionProfile;
19
+ readonly provider: string;
20
+ readonly model: string;
21
+ readonly thinkingLevel: ThinkingLevel;
22
+ readonly mcpServers?: McpServers;
23
+ }
24
+ export declare function toResolvedExecutionProfile(profile: ConfiguredExecutionProfile): ResolvedExecutionProfile;
25
+ /** A small read-only map implementation which has no mutating methods at runtime. */
26
+ export declare class ImmutableMap<K, V> implements ReadonlyMap<K, V> {
27
+ private readonly entriesMap;
28
+ constructor(entries: Iterable<readonly [K, V]>);
29
+ get size(): number;
30
+ get(key: K): V | undefined;
31
+ has(key: K): boolean;
32
+ entries(): MapIterator<[K, V]>;
33
+ keys(): MapIterator<K>;
34
+ values(): MapIterator<V>;
35
+ forEach(callbackfn: (value: V, key: K, map: ReadonlyMap<K, V>) => void, thisArg?: unknown): void;
36
+ [Symbol.iterator](): MapIterator<[K, V]>;
37
+ }
@@ -0,0 +1,29 @@
1
+ import { snapshotMcpServers } from "./mcp-config.js";
2
+ export const ALIAS_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
3
+ export function toResolvedExecutionProfile(profile) {
4
+ return Object.freeze({
5
+ alias: profile.alias,
6
+ permissionProfile: profile.permissionProfile,
7
+ provider: profile.provider,
8
+ model: profile.model,
9
+ thinkingLevel: profile.thinkingLevel,
10
+ ...(profile.mcpServers === undefined ? {} : { mcpServers: snapshotMcpServers(profile.mcpServers) }),
11
+ });
12
+ }
13
+ /** A small read-only map implementation which has no mutating methods at runtime. */
14
+ export class ImmutableMap {
15
+ entriesMap;
16
+ constructor(entries) {
17
+ this.entriesMap = new Map(entries);
18
+ }
19
+ get size() { return this.entriesMap.size; }
20
+ get(key) { return this.entriesMap.get(key); }
21
+ has(key) { return this.entriesMap.has(key); }
22
+ entries() { return this.entriesMap.entries(); }
23
+ keys() { return this.entriesMap.keys(); }
24
+ values() { return this.entriesMap.values(); }
25
+ forEach(callbackfn, thisArg) {
26
+ this.entriesMap.forEach((value, key) => callbackfn.call(thisArg, value, key, this));
27
+ }
28
+ [Symbol.iterator]() { return this.entriesMap.entries(); }
29
+ }
package/dist/main.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/main.js ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
3
+ import { loadConfig } from "./config.js";
4
+ import { startupFailureDiagnostic } from "./process-diagnostics.js";
5
+ import { createServer } from "./server.js";
6
+ import { SessionRegistry } from "./session-registry.js";
7
+ import { SdkPiSessionAdapter } from "./sdk-pi-adapter.js";
8
+ import { shutdownAndTerminate } from "./shutdown.js";
9
+ function writeDiagnostic(diagnostic) {
10
+ process.stderr.write(JSON.stringify(diagnostic) + "\n");
11
+ }
12
+ async function main() {
13
+ let registry;
14
+ let handle;
15
+ try {
16
+ const config = await loadConfig();
17
+ registry = new SessionRegistry(new SdkPiSessionAdapter({ diagnosticSink: writeDiagnostic }), config.workspaces);
18
+ handle = serveStdio(() => createServer(registry, config.workspaces, config.executionProfiles, config.defaultExecutionProfile, config.configuration), { onerror: () => writeDiagnostic({ level: "error", event: "mcp_transport_error", stage: "transport", code: "mcp_transport_error" }) });
19
+ }
20
+ catch (error) {
21
+ writeDiagnostic(startupFailureDiagnostic(error));
22
+ process.exitCode = 1;
23
+ return;
24
+ }
25
+ let stopping = false;
26
+ const shutdown = async (exitCode) => {
27
+ if (stopping)
28
+ return;
29
+ stopping = true;
30
+ await shutdownAndTerminate(() => registry.shutdown(), () => handle.close(), exitCode, {
31
+ report: (outcome) => {
32
+ if (outcome !== "completed")
33
+ writeDiagnostic({
34
+ level: "error", event: "shutdown_failed", stage: "shutdown",
35
+ code: outcome === "timed_out" ? "shutdown_timeout" : "shutdown_failed",
36
+ });
37
+ },
38
+ terminate: (code) => process.exit(code),
39
+ });
40
+ };
41
+ process.once("SIGINT", () => { void shutdown(130); });
42
+ process.once("SIGTERM", () => { void shutdown(143); });
43
+ // On stdin EOF the SDK closes only its transport and server instance; sessions and
44
+ // their MCP children need the same bounded shutdown as a signal. The first trigger wins.
45
+ const endOfInput = () => { void shutdown(0); };
46
+ process.stdin.once("end", endOfInput);
47
+ process.stdin.once("close", endOfInput);
48
+ if (process.stdin.readableEnded || process.stdin.destroyed)
49
+ endOfInput();
50
+ }
51
+ await main();
@@ -0,0 +1,22 @@
1
+ /** Operator-owned configuration. MCP callers can select a profile, never supply this data. */
2
+ export interface McpToolGrant {
3
+ readonly name: string;
4
+ /** An explicit operator assertion, never inferred from an MCP annotation. */
5
+ readonly readOnly: boolean;
6
+ }
7
+ export interface McpServerConfig {
8
+ readonly command: string;
9
+ readonly args?: readonly string[];
10
+ /** Child environment variable -> existing host environment variable (not a secret value). */
11
+ readonly envFrom?: Readonly<Record<string, string>>;
12
+ readonly tools: Readonly<Record<string, McpToolGrant>>;
13
+ }
14
+ export type McpServers = Readonly<Record<string, McpServerConfig>>;
15
+ export declare const MAX_MCP_SERVERS = 8;
16
+ export declare const MAX_MCP_TOOLS = 64;
17
+ export declare function mcpToolName(server: string, tool: string): string;
18
+ /** Also used at the adapter boundary: a manually constructed profile must not bypass policy. */
19
+ export declare function isMcpServers(value: unknown, permissionProfile?: "read-only" | "coding"): value is McpServers;
20
+ export declare function snapshotMcpServers(servers: McpServers): McpServers;
21
+ /** Safe discovery metadata; never project commands, arguments, paths, or environment references. */
22
+ export declare function configuredMcpToolNames(servers: McpServers): string[];