mirror of
https://github.com/morluto/rea.git
synced 2026-11-13 10:12:50 +00:00
feat(identity): derive MCP surface metadata
This commit is contained in:
1 parent
964620a0a1
commit
2722722425
18 files changed
+1099
-70
No files matched your search
@@ -0,0 +1,193 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TOOL_CONTRACTS } from "../contracts/toolContracts.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
|
||||
const statusSchema = z.object({
|
||||
open: z.boolean(),
|
||||
kind: z.enum(["executable", "database", "archive", "artifact"]).optional(),
|
||||
format: z.string().optional(),
|
||||
capabilities: z.array(
|
||||
z.object({
|
||||
operation: z.string(),
|
||||
available: z.boolean(),
|
||||
reason: z.string().nullable(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type ToolAvailabilityReason =
|
||||
| "available"
|
||||
| "target_required"
|
||||
| "provider_missing"
|
||||
| "provider_unavailable"
|
||||
| "target_unsupported"
|
||||
| "unsupported_host"
|
||||
| "policy_disabled";
|
||||
|
||||
const ENHANCED_REQUIREMENTS: Readonly<Record<string, readonly string[]>> = {
|
||||
swift_classes: ["list_procedures"],
|
||||
get_objc_classes: ["list_names"],
|
||||
get_objc_protocols: ["list_names"],
|
||||
batch_decompile: ["procedure_pseudo_code"],
|
||||
get_call_graph: ["procedure_callees", "procedure_callers"],
|
||||
analyze_swift_types: ["list_procedures"],
|
||||
find_xrefs_to_name: ["list_names", "xrefs"],
|
||||
binary_overview: [
|
||||
"list_segments",
|
||||
"list_documents",
|
||||
"list_procedures",
|
||||
"list_strings",
|
||||
],
|
||||
analyze_function: ["analyze_function"],
|
||||
trace_feature: [
|
||||
"list_strings",
|
||||
"list_procedures",
|
||||
"xrefs",
|
||||
"resolve_containing_procedure",
|
||||
],
|
||||
};
|
||||
|
||||
/** Build stable per-operation availability without hiding familiar tools. */
|
||||
export const buildCapabilityInventory = (
|
||||
sessionStatus: JsonValue,
|
||||
policy: {
|
||||
readonly processCaptureEnabled: boolean;
|
||||
readonly evidenceFileRoots: number;
|
||||
},
|
||||
) => {
|
||||
const status = statusSchema.parse(sessionStatus);
|
||||
const descriptors = new Map(
|
||||
status.capabilities.map((descriptor) => [descriptor.operation, descriptor]),
|
||||
);
|
||||
return TOOL_CONTRACTS.map((contract) => {
|
||||
const availability = availabilityFor(
|
||||
contract.name,
|
||||
contract.kind,
|
||||
status.open,
|
||||
status.kind,
|
||||
descriptors,
|
||||
policy,
|
||||
);
|
||||
return {
|
||||
name: contract.name,
|
||||
surface: contract.kind,
|
||||
available: availability.reason === "available",
|
||||
reason: availability.reason,
|
||||
remediation: availability.remediation,
|
||||
annotations: {
|
||||
read_only: contract.annotations.readOnlyHint ?? false,
|
||||
destructive: contract.annotations.destructiveHint ?? false,
|
||||
idempotent: contract.annotations.idempotentHint ?? false,
|
||||
open_world: contract.annotations.openWorldHint ?? true,
|
||||
},
|
||||
};
|
||||
}).sort((left, right) => left.name.localeCompare(right.name));
|
||||
};
|
||||
|
||||
const availabilityFor = (
|
||||
name: string,
|
||||
kind: (typeof TOOL_CONTRACTS)[number]["kind"],
|
||||
targetOpen: boolean,
|
||||
targetKind: "executable" | "database" | "archive" | "artifact" | undefined,
|
||||
descriptors: ReadonlyMap<
|
||||
string,
|
||||
{ readonly available: boolean; readonly reason: string | null }
|
||||
>,
|
||||
policy: {
|
||||
readonly processCaptureEnabled: boolean;
|
||||
readonly evidenceFileRoots: number;
|
||||
},
|
||||
): {
|
||||
readonly reason: ToolAvailabilityReason;
|
||||
readonly remediation: string | null;
|
||||
} => {
|
||||
if (name === "capture_process_scenario" && !policy.processCaptureEnabled)
|
||||
return {
|
||||
reason: "policy_disabled",
|
||||
remediation:
|
||||
"Enable or grant process_capture within the administrator ceiling.",
|
||||
};
|
||||
if (name === "import_evidence_bundle" && policy.evidenceFileRoots === 0)
|
||||
return {
|
||||
reason: "policy_disabled",
|
||||
remediation: "Configure or grant an evidence_read root.",
|
||||
};
|
||||
if (kind === "session") return { reason: "available", remediation: null };
|
||||
if (!targetOpen)
|
||||
return {
|
||||
reason: "target_required",
|
||||
remediation: "Call open_binary with a supported local target.",
|
||||
};
|
||||
if (
|
||||
targetKind !== undefined &&
|
||||
targetKind !== "executable" &&
|
||||
targetKind !== "database" &&
|
||||
(kind === "official-proxy" || kind === "enhanced")
|
||||
)
|
||||
return {
|
||||
reason: "target_unsupported",
|
||||
remediation:
|
||||
"Inventory or extract a native executable, then call open_binary on that executable.",
|
||||
};
|
||||
if (kind === "enhanced") return composedAvailability(name, descriptors);
|
||||
const descriptor = descriptors.get(name);
|
||||
if (descriptor === undefined)
|
||||
return {
|
||||
reason: "provider_missing",
|
||||
remediation:
|
||||
"Install or configure a provider that declares this operation.",
|
||||
};
|
||||
if (
|
||||
!descriptor.available &&
|
||||
descriptor.reason?.toLowerCase().includes("require macos") === true
|
||||
)
|
||||
return {
|
||||
reason: "unsupported_host",
|
||||
remediation: descriptor.reason,
|
||||
};
|
||||
return descriptor.available
|
||||
? { reason: "available", remediation: null }
|
||||
: {
|
||||
reason: "provider_unavailable",
|
||||
remediation:
|
||||
descriptor.reason ?? "Choose another target or configured provider.",
|
||||
};
|
||||
};
|
||||
|
||||
const composedAvailability = (
|
||||
name: string,
|
||||
descriptors: ReadonlyMap<
|
||||
string,
|
||||
{ readonly available: boolean; readonly reason: string | null }
|
||||
>,
|
||||
): {
|
||||
readonly reason: ToolAvailabilityReason;
|
||||
readonly remediation: string | null;
|
||||
} => {
|
||||
const requirements = ENHANCED_REQUIREMENTS[name];
|
||||
if (requirements === undefined)
|
||||
return {
|
||||
reason: "provider_missing",
|
||||
remediation: "No provider composition is declared for this operation.",
|
||||
};
|
||||
const missing = requirements.find((operation) => !descriptors.has(operation));
|
||||
if (missing !== undefined)
|
||||
return {
|
||||
reason: "provider_missing",
|
||||
remediation: `Configure a provider for required operation ${missing}.`,
|
||||
};
|
||||
const unavailable = requirements
|
||||
.map((operation) => descriptors.get(operation))
|
||||
.find((descriptor) => descriptor?.available === false);
|
||||
if (unavailable !== undefined)
|
||||
return {
|
||||
reason:
|
||||
unavailable.reason?.toLowerCase().includes("require macos") === true
|
||||
? "unsupported_host"
|
||||
: "provider_unavailable",
|
||||
remediation:
|
||||
unavailable.reason ?? "Choose another target or configured provider.",
|
||||
};
|
||||
return { reason: "available", remediation: null };
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
import { access, readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { parse as parseToml } from "smol-toml";
|
||||
import { z } from "zod";
|
||||
|
||||
import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
import { supportedClients } from "./SupportedClients.js";
|
||||
|
||||
export interface ClientRegistrationStatus {
|
||||
readonly client: string;
|
||||
readonly config_path: string;
|
||||
readonly command: readonly string[];
|
||||
readonly state: "aligned" | "stale" | "missing" | "invalid";
|
||||
readonly remediation: string | null;
|
||||
}
|
||||
|
||||
const objectSchema = z.record(z.string(), z.unknown());
|
||||
const registrationSchema = z
|
||||
.object({ command: z.string().min(1), args: z.array(z.string()).default([]) })
|
||||
.passthrough();
|
||||
|
||||
/** Inspect supported client registrations without reading their environment. */
|
||||
export const readClientRegistrationStatuses = async (
|
||||
home: string,
|
||||
currentCommandPath: string = resolve(process.argv[1] ?? "unknown"),
|
||||
): Promise<readonly ClientRegistrationStatus[]> => {
|
||||
const statuses: ClientRegistrationStatus[] = [];
|
||||
for (const client of supportedClients(home)) {
|
||||
if (client.format === "unsupported" || !(await exists(client.markerPath)))
|
||||
continue;
|
||||
try {
|
||||
const content = await readFile(client.configPath, "utf8");
|
||||
const document = objectSchema.parse(
|
||||
client.format === "toml" ? parseToml(content) : JSON.parse(content),
|
||||
);
|
||||
const servers = objectSchema.parse(
|
||||
document[client.format === "toml" ? "mcp_servers" : "mcpServers"] ?? {},
|
||||
);
|
||||
const raw = servers[PRODUCT_IDENTITY.mcpServerKey];
|
||||
if (raw === undefined) {
|
||||
statuses.push(status(client.name, client.configPath, [], "missing"));
|
||||
continue;
|
||||
}
|
||||
const registration = registrationSchema.parse(raw);
|
||||
const command = [registration.command, ...registration.args];
|
||||
statuses.push(
|
||||
status(
|
||||
client.name,
|
||||
client.configPath,
|
||||
command,
|
||||
registrationAligned(command, currentCommandPath)
|
||||
? "aligned"
|
||||
: "stale",
|
||||
),
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
statuses.push(
|
||||
status(
|
||||
client.name,
|
||||
client.configPath,
|
||||
[],
|
||||
isMissing(cause) ? "missing" : "invalid",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return statuses.sort((left, right) =>
|
||||
left.client.localeCompare(right.client),
|
||||
);
|
||||
};
|
||||
|
||||
const registrationAligned = (
|
||||
command: readonly string[],
|
||||
currentCommandPath: string,
|
||||
): boolean => {
|
||||
if (
|
||||
command.length === 4 &&
|
||||
command[0] === "npx" &&
|
||||
command[1] === "-y" &&
|
||||
command[2] === PRODUCT_IDENTITY.packageName &&
|
||||
command[3] === "mcp"
|
||||
)
|
||||
return true;
|
||||
return (
|
||||
command.length === 2 &&
|
||||
command[1] === "mcp" &&
|
||||
resolve(command[0] ?? "") === currentCommandPath
|
||||
);
|
||||
};
|
||||
|
||||
const status = (
|
||||
client: string,
|
||||
configPath: string,
|
||||
command: readonly string[],
|
||||
state: ClientRegistrationStatus["state"],
|
||||
): ClientRegistrationStatus => ({
|
||||
client,
|
||||
config_path: configPath,
|
||||
command,
|
||||
state,
|
||||
remediation:
|
||||
state === "aligned"
|
||||
? null
|
||||
: "Run rea setup to refresh this registration, then restart the client.",
|
||||
});
|
||||
|
||||
const exists = async (path: string | undefined): Promise<boolean> => {
|
||||
if (path === undefined) return false;
|
||||
try {
|
||||
await access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const isMissing = (cause: unknown): boolean =>
|
||||
cause instanceof Error && "code" in cause && cause.code === "ENOENT";
|
||||
+113
-2
@@ -1,6 +1,5 @@
|
||||
import { constants } from "node:fs";
|
||||
import { access } from "node:fs/promises";
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { access, readFile, readdir } from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { execFile } from "node:child_process";
|
||||
@@ -16,6 +15,12 @@ import {
|
||||
readLinuxDistribution,
|
||||
type LinuxDistribution,
|
||||
} from "./LinuxHopper.js";
|
||||
import { CATALOG_IDENTITY } from "../catalogIdentity.js";
|
||||
import { PRODUCT_IDENTITY, SDK_IDENTITY } from "../identity.js";
|
||||
import {
|
||||
readClientRegistrationStatuses,
|
||||
type ClientRegistrationStatus,
|
||||
} from "./ClientRegistrationStatus.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const DEFAULT_HOPPER =
|
||||
@@ -48,6 +53,9 @@ export interface DoctorHost {
|
||||
linuxDemoRuntimeReady(): Promise<boolean>;
|
||||
brewHopperPath(): Promise<string | undefined>;
|
||||
manualHopperPaths(): Promise<readonly string[]>;
|
||||
installationPaths?(): Promise<readonly string[]>;
|
||||
installedSkillVersion?(): Promise<string | undefined>;
|
||||
clientRegistrations?(): Promise<readonly ClientRegistrationStatus[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,6 +70,26 @@ export const runDoctor = async (
|
||||
readonly healthy: boolean;
|
||||
readonly hopperPath?: string;
|
||||
readonly checks: readonly DoctorCheck[];
|
||||
readonly identity?: {
|
||||
readonly cli_package_version: string;
|
||||
readonly expected_skill_version: string;
|
||||
readonly sdk: typeof SDK_IDENTITY;
|
||||
readonly catalog: typeof CATALOG_IDENTITY;
|
||||
readonly live_server: {
|
||||
readonly state: "unknown";
|
||||
readonly remediation: string;
|
||||
};
|
||||
readonly installations: {
|
||||
readonly paths: readonly string[];
|
||||
readonly state: "single" | "multiple" | "unknown";
|
||||
};
|
||||
readonly skill: {
|
||||
readonly installed_version: string | null;
|
||||
readonly state: "aligned" | "stale" | "unknown";
|
||||
readonly remediation: string | null;
|
||||
};
|
||||
readonly registrations: readonly ClientRegistrationStatus[];
|
||||
};
|
||||
}> => {
|
||||
const checks: DoctorCheck[] = [];
|
||||
checks.push(
|
||||
@@ -148,10 +176,59 @@ export const runDoctor = async (
|
||||
classification: "config_drift",
|
||||
}),
|
||||
);
|
||||
const installationPaths = (await host.installationPaths?.()) ?? [];
|
||||
const installedSkillVersion = await host.installedSkillVersion?.();
|
||||
const registrations = (await host.clientRegistrations?.()) ?? [];
|
||||
for (const registration of registrations)
|
||||
if (registration.state !== "aligned")
|
||||
checks.push({
|
||||
name: `registration:${registration.client}`,
|
||||
ok: false,
|
||||
classification: "config_drift",
|
||||
detail: registration.config_path,
|
||||
remediation:
|
||||
registration.remediation ??
|
||||
"Run rea setup, then restart the affected client.",
|
||||
});
|
||||
return {
|
||||
healthy: checks.every(({ ok }) => ok),
|
||||
...(hopperPath === undefined ? {} : { hopperPath }),
|
||||
checks,
|
||||
identity: {
|
||||
cli_package_version: PRODUCT_IDENTITY.packageVersion,
|
||||
expected_skill_version: PRODUCT_IDENTITY.skillVersion,
|
||||
sdk: SDK_IDENTITY,
|
||||
catalog: CATALOG_IDENTITY,
|
||||
live_server: {
|
||||
state: "unknown",
|
||||
remediation:
|
||||
"Compare rea://server/identity or binary_session from the active client; an on-disk registration cannot prove the running server version.",
|
||||
},
|
||||
installations: {
|
||||
paths: installationPaths,
|
||||
state:
|
||||
installationPaths.length === 0
|
||||
? "unknown"
|
||||
: installationPaths.length === 1
|
||||
? "single"
|
||||
: "multiple",
|
||||
},
|
||||
skill: {
|
||||
installed_version: installedSkillVersion ?? null,
|
||||
state:
|
||||
installedSkillVersion === undefined
|
||||
? "unknown"
|
||||
: installedSkillVersion === PRODUCT_IDENTITY.skillVersion
|
||||
? "aligned"
|
||||
: "stale",
|
||||
remediation:
|
||||
installedSkillVersion === undefined ||
|
||||
installedSkillVersion === PRODUCT_IDENTITY.skillVersion
|
||||
? null
|
||||
: "Run rea setup to update the installed REA skill.",
|
||||
},
|
||||
registrations,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -241,10 +318,44 @@ export const systemDoctorHost = (): DoctorHost => ({
|
||||
}
|
||||
return paths;
|
||||
},
|
||||
async installationPaths() {
|
||||
try {
|
||||
const command = process.platform === "win32" ? "where" : "which";
|
||||
const arguments_ = process.platform === "win32" ? ["rea"] : ["-a", "rea"];
|
||||
return uniqueLines((await execFileAsync(command, arguments_)).stdout);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
async installedSkillVersion() {
|
||||
try {
|
||||
const content = await readFile(
|
||||
join(
|
||||
homedir(),
|
||||
".agents/skills",
|
||||
PRODUCT_IDENTITY.skillName,
|
||||
"SKILL.md",
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
return /^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(content)?.[1];
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
clientRegistrations: () => readClientRegistrationStatuses(homedir()),
|
||||
});
|
||||
|
||||
const parseMajor = (version: string): number =>
|
||||
Number.parseInt(version.split(".")[0] ?? "0", 10);
|
||||
const uniqueLines = (value: string): string[] => [
|
||||
...new Set(
|
||||
value
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0),
|
||||
),
|
||||
];
|
||||
const check = (
|
||||
name: string,
|
||||
ok: boolean,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { z } from "zod";
|
||||
import writeFileAtomic from "write-file-atomic";
|
||||
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
} from "./LinuxHopper.js";
|
||||
import { installMacHopper } from "./MacHopper.js";
|
||||
import { configureDetectedClients } from "./SetupClients.js";
|
||||
import { supportedClients, type SetupClient } from "./SupportedClients.js";
|
||||
export type { SetupClient } from "./SupportedClients.js";
|
||||
import { installCanonicalSkill } from "./SetupSkill.js";
|
||||
import { setupPlan } from "./SetupPlan.js";
|
||||
import {
|
||||
@@ -39,13 +41,6 @@ const registrationCommand = (): readonly string[] =>
|
||||
? ["npx", "-y", PRODUCT_IDENTITY.packageName, "mcp"]
|
||||
: [resolve(process.argv[1] ?? PRODUCT_IDENTITY.cliBinary), "mcp"];
|
||||
|
||||
/** A detected agent configuration owned by setup. */
|
||||
export interface SetupClient {
|
||||
readonly name: string;
|
||||
readonly configPath: string;
|
||||
readonly markerPath?: string;
|
||||
readonly format?: "json" | "toml" | "unsupported";
|
||||
}
|
||||
/** Result of one backup/write/readback transaction. */
|
||||
export type ClientConfigurationResult =
|
||||
| {
|
||||
@@ -282,50 +277,6 @@ export const detectClients = async (
|
||||
return detected;
|
||||
};
|
||||
|
||||
/** Describe every client location that setup or uninstall may own. */
|
||||
export const supportedClients = (home: string): readonly SetupClient[] => [
|
||||
{
|
||||
name: "claude_code",
|
||||
configPath: join(home, ".claude.json"),
|
||||
markerPath: join(home, ".claude"),
|
||||
},
|
||||
{
|
||||
name: "claude_desktop",
|
||||
configPath: join(
|
||||
home,
|
||||
"Library/Application Support/Claude/claude_desktop_config.json",
|
||||
),
|
||||
markerPath: join(home, "Library/Application Support/Claude"),
|
||||
},
|
||||
{
|
||||
name: "codex",
|
||||
configPath: join(home, ".codex/config.toml"),
|
||||
markerPath: join(home, ".codex"),
|
||||
format: "toml" as const,
|
||||
},
|
||||
{
|
||||
name: "cursor",
|
||||
configPath: join(home, ".cursor/mcp.json"),
|
||||
markerPath: join(home, ".cursor"),
|
||||
},
|
||||
{
|
||||
name: "gemini_cli",
|
||||
configPath: join(home, ".gemini/settings.json"),
|
||||
markerPath: join(home, ".gemini"),
|
||||
},
|
||||
{
|
||||
name: "windsurf",
|
||||
configPath: join(home, ".codeium/windsurf/mcp_config.json"),
|
||||
markerPath: join(home, ".codeium/windsurf"),
|
||||
},
|
||||
{
|
||||
name: "devin",
|
||||
configPath: join(home, ".devin"),
|
||||
markerPath: join(home, ".devin"),
|
||||
format: "unsupported" as const,
|
||||
},
|
||||
];
|
||||
|
||||
/** Back up, atomically update, and semantically read back one JSON MCP configuration. */
|
||||
export const configureJsonClient = async (
|
||||
client: SetupClient,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { join } from "node:path";
|
||||
|
||||
/** One supported client configuration location. */
|
||||
export interface SetupClient {
|
||||
readonly name: string;
|
||||
readonly configPath: string;
|
||||
readonly markerPath?: string;
|
||||
readonly format?: "json" | "toml" | "unsupported";
|
||||
}
|
||||
|
||||
/** Describe every client location that setup, doctor, or uninstall may inspect. */
|
||||
export const supportedClients = (home: string): readonly SetupClient[] => [
|
||||
{
|
||||
name: "claude_code",
|
||||
configPath: join(home, ".claude.json"),
|
||||
markerPath: join(home, ".claude"),
|
||||
},
|
||||
{
|
||||
name: "claude_desktop",
|
||||
configPath: join(
|
||||
home,
|
||||
"Library/Application Support/Claude/claude_desktop_config.json",
|
||||
),
|
||||
markerPath: join(home, "Library/Application Support/Claude"),
|
||||
},
|
||||
{
|
||||
name: "codex",
|
||||
configPath: join(home, ".codex/config.toml"),
|
||||
markerPath: join(home, ".codex"),
|
||||
format: "toml",
|
||||
},
|
||||
{
|
||||
name: "cursor",
|
||||
configPath: join(home, ".cursor/mcp.json"),
|
||||
markerPath: join(home, ".cursor"),
|
||||
},
|
||||
{
|
||||
name: "gemini_cli",
|
||||
configPath: join(home, ".gemini/settings.json"),
|
||||
markerPath: join(home, ".gemini"),
|
||||
},
|
||||
{
|
||||
name: "windsurf",
|
||||
configPath: join(home, ".codeium/windsurf/mcp_config.json"),
|
||||
markerPath: join(home, ".codeium/windsurf"),
|
||||
},
|
||||
{
|
||||
name: "devin",
|
||||
configPath: join(home, ".devin"),
|
||||
markerPath: join(home, ".devin"),
|
||||
format: "unsupported",
|
||||
},
|
||||
];
|
||||
@@ -7,7 +7,7 @@ import writeFileAtomic from "write-file-atomic";
|
||||
import { z } from "zod";
|
||||
|
||||
import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
import { supportedClients, type SetupClient } from "./Setup.js";
|
||||
import { supportedClients, type SetupClient } from "./SupportedClients.js";
|
||||
|
||||
interface ManagedPathStats {
|
||||
isSymbolicLink(): boolean;
|
||||
|
||||
@@ -52,6 +52,8 @@ export type UpgradeResult =
|
||||
readonly versionCheck: "available" | "unavailable";
|
||||
readonly installMethod: "npm";
|
||||
readonly command: typeof UPGRADE_COMMAND;
|
||||
readonly clientRestartRequired: true;
|
||||
readonly remediation: string;
|
||||
}
|
||||
| {
|
||||
readonly status: "failed";
|
||||
@@ -102,6 +104,9 @@ export const runUpgrade = async (
|
||||
versionCheck: latestVersion === undefined ? "unavailable" : "available",
|
||||
installMethod: "npm",
|
||||
command: UPGRADE_COMMAND,
|
||||
clientRestartRequired: true,
|
||||
remediation:
|
||||
"Rerun rea setup to refresh registrations and the skill, then restart clients that may retain an older MCP server.",
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user