Extract MCP toolkit installer support seams

This commit is contained in:
OpenPets Dev 2026-06-19 06:23:38 +00:00
parent 13e23c60c9
commit 5caf9b1c39
8 changed files with 536 additions and 333 deletions

View file

@ -64,6 +64,7 @@ const behaviorTests = [
".test-dist/tests/knowledge-store-core-seam.test.js",
".test-dist/tests/knowledge-store.test.js",
".test-dist/tests/control-center-service-barrels.test.js",
".test-dist/tests/mcp-toolkit-installer-seams.test.js",
".test-dist/tests/mcp-toolkit-catalog.test.js",
".test-dist/tests/integrations-view-toolkit-split.test.js",
".test-dist/tests/integrations-view-state.test.js",

View file

@ -147,6 +147,9 @@ const promptWindowPreloadSource = readFileSync(join(appDir, "prompt-window-prelo
const reactionMessagesSource = readFileSync(join(appDir, "src", "reaction-messages.ts"), "utf8");
const displaySource = readFileSync(join(appDir, "src", "display.ts"), "utf8");
const updateCheckerSource = readFileSync(join(appDir, "src", "update-checker.ts"), "utf8");
const mcpToolkitInstallerSource = readFileSync(join(appDir, "src", "mcp-toolkit-installer.ts"), "utf8");
const mcpToolkitInstallerSupportSource = readFileSync(join(appDir, "src", "mcp-toolkit-installer-support.ts"), "utf8");
const mcpToolkitInstallerCommandsSource = readFileSync(join(appDir, "src", "mcp-toolkit-installer-commands.ts"), "utf8");
const traySource = readFileSync(join(appDir, "src", "tray.ts"), "utf8");
const enCatalogSource = readFileSync(join(appDir, "src", "i18n", "locales", "en.ts"), "utf8");
const windowsSource = readFileSync(join(appDir, "src", "windows.ts"), "utf8");
@ -582,6 +585,12 @@ assert.match(controlCenterIntegrationsMcpToolkitSectionSource, /export function
assert.match(controlCenterIntegrationsMcpToolkitSectionSource, /buildPersistentToolkitBundle/, "Control Center integrations toolkit seam must own persistent bundle composition.");
assert.match(controlCenterIntegrationsCombinedSource, /Persistent Full Access/, "Control Center integrations seam must retain the persistent toolkit install path.");
assert.match(controlCenterIntegrationsCombinedSource, /pi install npm:@familiaros\/pi/, "Control Center integrations seam must retain Pi install guidance.");
assert.match(mcpToolkitInstallerSource, /from "\.\/mcp-toolkit-installer-support(?:\.js)?"/, "MCP toolkit installer must import the extracted support seam.");
assert.match(mcpToolkitInstallerSource, /from "\.\/mcp-toolkit-installer-commands(?:\.js)?"/, "MCP toolkit installer must import the extracted command seam.");
assert.match(mcpToolkitInstallerSupportSource, /export function getSupportedPersistentServers/, "MCP toolkit installer support seam must export managed server definitions.");
assert.match(mcpToolkitInstallerSupportSource, /export function buildInstallCommands/, "MCP toolkit installer support seam must export bundle command shaping.");
assert.match(mcpToolkitInstallerCommandsSource, /export function runCommand/, "MCP toolkit installer command seam must export process execution helpers.");
assert.match(mcpToolkitInstallerCommandsSource, /export async function runCommandWithCandidates/, "MCP toolkit installer command seam must export command discovery helpers.");
assert.match(openApiChatSource, /from "\.\/openapi-chat-settings(?:\.js)?"/, "OpenAPI chat must import the extracted settings seam.");
assert.match(openApiChatSource, /from "\.\/openapi-chat-prompt-flows(?:\.js)?"/, "OpenAPI chat must import the extracted prompt-flow seam.");
assert.match(openApiChatPromptFlowsSource, /export async function sendOpenApiChatPromptPlainFlow/, "OpenAPI chat prompt-flow seam must export the plain prompt runner.");

View file

@ -205,6 +205,9 @@ plugin-service-local-support.ts → plugin-local-loader.ts validates selected fo
- `agent-setup-actions.ts`: Extracted action execution shell for hook handling plus Claude MCP add/remove orchestration
- `agent-setup-actions-tooling.ts`: Extracted OpenCode/Cursor setup loading, global config actions, and FamiliarOS MCP server health checks
- `agent-setup-support.ts`: Extracted MCP preview, Claude memory safety wrappers, and bounded action journal helpers for agent setup
- `mcp-toolkit-installer.ts`: Public MCP toolkit installer facade for persistent host-agent server setup and managed install result shaping
- `mcp-toolkit-installer-support.ts`: Extracted MCP toolkit bundle/server-definition, argument, note, and command-candidate helpers
- `mcp-toolkit-installer-commands.ts`: Extracted MCP toolkit PATH/process execution, timeout, and command-failure shaping helpers
- `assets.ts`: Tray icon loading with generated fallback
- `display.ts`: Screen geometry helpers, familiar window positioning
- `prompt-window.ts`: Floating prompt window lifecycle, bounded placement/resize logic, and main-process prompt/settings/knowledge IPC handlers

View file

@ -0,0 +1,199 @@
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { join } from "node:path";
import { app } from "electron";
export interface CommandResult {
readonly ok: boolean;
readonly exitCode: number | null;
readonly stdout: string;
readonly stderr: string;
readonly error?: string;
}
const commandTimeoutMs = 20_000;
const maxOutputChars = 6_000;
export async function runCommandWithCandidates(
candidates: readonly string[],
): Promise<CommandResult> {
let lastResult: CommandResult = {
ok: false,
exitCode: null,
stdout: "",
stderr: "",
error: "Command was not found.",
};
for (const candidate of candidates) {
const result = await runCommand(candidate, ["--version"]);
if (result.ok || !looksLikeMissingCommand(result)) {
return result;
}
lastResult = result;
}
return lastResult;
}
export function runCommand(
command: string,
args: readonly string[],
): Promise<CommandResult> {
return new Promise((resolve) => {
const shellCommand =
process.platform === "win32" && command.toLowerCase().endsWith(".cmd")
? "cmd.exe"
: command;
const shellArgs =
process.platform === "win32" && command.toLowerCase().endsWith(".cmd")
? ["/d", "/s", "/c", command, ...args]
: [...args];
let child;
try {
child = spawn(shellCommand, shellArgs, {
cwd: app.getPath("home"),
env: createCommandEnv(),
windowsHide: true,
shell: false,
});
} catch (error) {
resolve({
ok: false,
exitCode: null,
stdout: "",
stderr: "",
error:
error instanceof Error ? error.message : "Command failed to start.",
});
return;
}
let stdout = "";
let stderr = "";
let settled = false;
const timer = setTimeout(() => {
if (settled) return;
settled = true;
child.kill();
resolve({
ok: false,
exitCode: null,
stdout: truncateOutput(stdout),
stderr: truncateOutput(stderr),
error: "Command timed out.",
});
}, commandTimeoutMs);
child.stdout?.on("data", (chunk: Buffer) => {
stdout = truncateOutput(stdout + chunk.toString("utf8"));
});
child.stderr?.on("data", (chunk: Buffer) => {
stderr = truncateOutput(stderr + chunk.toString("utf8"));
});
child.on("error", (error) => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve({
ok: false,
exitCode: null,
stdout: truncateOutput(stdout),
stderr: truncateOutput(stderr),
error: error.message,
});
});
child.on("close", (code) => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve({
ok: code === 0,
exitCode: code,
stdout: truncateOutput(stdout),
stderr: truncateOutput(stderr),
});
});
});
}
export function summarizeCommandFailure(result: CommandResult): string {
const detail =
result.stderr ||
result.stdout ||
result.error ||
`exit code ${result.exitCode ?? "unknown"}`;
return truncateOutput(detail).trim() || "command failed.";
}
function createCommandEnv(): NodeJS.ProcessEnv {
const separator = process.platform === "win32" ? ";" : ":";
const existingPath = process.env.PATH ?? "";
return {
...process.env,
PATH: dedupePathEntries(
[existingPath, ...getExtraCommandPaths()],
separator,
).join(separator),
};
}
function getExtraCommandPaths(): readonly string[] {
if (process.platform === "win32") return [];
const home = app.getPath("home");
const env = process.env;
return filterExistingPaths([
"/opt/homebrew/bin",
"/opt/homebrew/sbin",
"/usr/local/bin",
"/usr/local/sbin",
"/usr/bin",
"/bin",
"/usr/sbin",
"/sbin",
join(home, "bin"),
join(home, ".local", "bin"),
join(home, ".opencode", "bin"),
join(env.VOLTA_HOME || join(home, ".volta"), "bin"),
join(env.BUN_INSTALL || join(home, ".bun"), "bin"),
join(
env.MISE_DATA_DIR || join(home, ".local", "share", "mise"),
"shims",
),
join(env.ASDF_DATA_DIR || join(home, ".asdf"), "shims"),
env.PNPM_HOME,
join(home, ".local", "share", "pnpm"),
join(home, "Library", "pnpm"),
join(env.NVM_DIR || join(home, ".nvm"), "current", "bin"),
]);
}
function filterExistingPaths(
paths: readonly (string | undefined)[],
): readonly string[] {
return paths.filter((path): path is string => Boolean(path && existsSync(path)));
}
function dedupePathEntries(
paths: readonly string[],
separator: string,
): readonly string[] {
const seen = new Set<string>();
const entries: string[] = [];
for (const path of paths.flatMap((value) => value.split(separator)).filter(Boolean)) {
if (seen.has(path)) continue;
seen.add(path);
entries.push(path);
}
return entries;
}
function looksLikeMissingCommand(result: CommandResult): boolean {
return Boolean(result.error && /ENOENT|not found/i.test(result.error));
}
function truncateOutput(value: string): string {
return value.length > maxOutputChars
? value.slice(value.length - maxOutputChars)
: value;
}

View file

@ -0,0 +1,197 @@
export type McpToolkitPersistentTarget = "claude-user" | "codex-global";
export interface McpToolkitBundle {
readonly label: string;
readonly language: "bash";
readonly value: string;
readonly description: string;
}
export interface McpToolkitInstallItemResult {
readonly id: string;
readonly name: string;
readonly status: "installed" | "skipped";
readonly detail: string;
}
export interface McpToolkitInstallResult {
readonly target: McpToolkitPersistentTarget;
readonly label: string;
readonly installed: readonly McpToolkitInstallItemResult[];
readonly skipped: readonly McpToolkitInstallItemResult[];
readonly notes: readonly string[];
}
export interface McpToolkitServerDefinition {
readonly id: string;
readonly name: string;
readonly description: string;
readonly command: readonly string[];
readonly requiredRuntime: "npx" | "uvx";
readonly note?: string;
}
export function buildPersistentToolkitBundleLabel(
target: McpToolkitPersistentTarget,
): string {
return target === "claude-user"
? "Claude Code user-scope install bundle"
: "Codex CLI global install bundle";
}
export function buildPersistentToolkitBundleDescription(
target: McpToolkitPersistentTarget,
): string {
return target === "claude-user"
? "Run once to install the supported persistent baseline into Claude Code at user scope."
: "Run once to install the supported persistent baseline into Codex CLI globally.";
}
export function buildPersistentToolkitInstallLabel(
target: McpToolkitPersistentTarget,
): string {
return target === "claude-user"
? "Claude Code user scope"
: "Codex CLI global";
}
export function buildPersistentToolkitNotes(
homeDir: string,
uvxAvailable: boolean,
): readonly string[] {
const notes = [
`Filesystem access is scoped to ${formatUserPath(homeDir, homeDir)} by default.`,
"Git, GitHub, databases, Docker, shell, SSH, and reverse-engineering lanes still stay manual because they need repo paths, auth, or stronger trust decisions.",
];
return uvxAvailable
? notes
: [
...notes,
"Browser Use and Fetch / Web were skipped because `uvx` is not installed on this machine.",
];
}
export function buildInstallCommands(
target: McpToolkitPersistentTarget,
hostCommand: string,
servers: readonly McpToolkitServerDefinition[],
): readonly (readonly string[])[] {
return servers.map((server) => [hostCommand, ...buildAddArgs(target, server)]);
}
export function getSupportedPersistentServers(
homeDir: string,
): readonly McpToolkitServerDefinition[] {
return [
{
id: "familiaros-filesystem",
name: "Filesystem",
description: "Persistent filesystem access scoped to your home directory.",
command: ["npx", "-y", "@modelcontextprotocol/server-filesystem", homeDir],
requiredRuntime: "npx",
note: "Scoped to your home directory by default. Narrow it later if you want a tighter boundary.",
},
{
id: "familiaros-playwright",
name: "Playwright",
description: "Browser automation and browser validation tools.",
command: ["npx", "@playwright/mcp@latest"],
requiredRuntime: "npx",
},
{
id: "familiaros-memory",
name: "Memory",
description: "Persistent project-memory tools for the host agent.",
command: ["npx", "-y", "@modelcontextprotocol/server-memory"],
requiredRuntime: "npx",
},
{
id: "familiaros-context7",
name: "Context7 / Docs",
description: "Up-to-date library and framework docs lookup.",
command: ["npx", "-y", "@upstash/context7-mcp"],
requiredRuntime: "npx",
},
{
id: "familiaros-fetch",
name: "Fetch / Web",
description: "Lightweight web retrieval without full browser automation.",
command: ["uvx", "mcp-server-fetch"],
requiredRuntime: "uvx",
},
{
id: "familiaros-sequential-thinking",
name: "Sequential Thinking",
description: "Structured planning and branching reasoning tools.",
command: ["npx", "-y", "@modelcontextprotocol/server-sequential-thinking"],
requiredRuntime: "npx",
},
{
id: "familiaros-browser-use",
name: "Browser Use",
description: "Higher-level browser task execution for agentic web work.",
command: ["uvx", "--from", "browser-use[cli]", "browser-use", "--mcp"],
requiredRuntime: "uvx",
note: "The entry is installed, but Browser Use still needs its own runtime credential in the host environment.",
},
];
}
export function buildAddArgs(
target: McpToolkitPersistentTarget,
server: McpToolkitServerDefinition,
): readonly string[] {
return target === "claude-user"
? ["mcp", "add", "--scope", "user", server.id, "--", ...server.command]
: ["mcp", "add", server.id, "--", ...server.command];
}
export function buildRemoveArgs(
target: McpToolkitPersistentTarget,
serverId: string,
): readonly string[] {
return target === "claude-user"
? ["mcp", "remove", "--scope", "user", serverId]
: ["mcp", "remove", serverId];
}
export function getHostCommandCandidates(
target: McpToolkitPersistentTarget,
preferredClaudeCommand: string,
): readonly string[] {
if (target === "claude-user") {
return getCommandCandidates(preferredClaudeCommand, "claude");
}
return getCommandCandidates("codex", "codex");
}
export function getRuntimeCommandCandidates(
command: "npx" | "uvx",
): readonly string[] {
return getCommandCandidates(command, command);
}
export function quoteShellArg(value: string): string {
if (/^[A-Za-z0-9_./:@-]+$/.test(value)) return value;
return JSON.stringify(value);
}
function formatUserPath(path: string, homeDir: string): string {
return path.replace(homeDir, "~");
}
function getCommandCandidates(
preferred: string,
baseName: string,
): readonly string[] {
if (preferred !== baseName) {
return preferred.toLowerCase().endsWith(".cmd")
? [preferred]
: [
preferred,
...(process.platform === "win32" ? [`${preferred}.cmd`] : []),
];
}
if (process.platform === "win32") return [baseName, `${baseName}.cmd`];
return [baseName];
}

View file

@ -1,94 +1,86 @@
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { join } from "node:path";
import { app } from "electron";
import { getAppStateSnapshot } from "./app-state.js";
import {
runCommand,
runCommandWithCandidates,
summarizeCommandFailure,
} from "./mcp-toolkit-installer-commands.js";
import {
buildAddArgs,
buildInstallCommands,
buildPersistentToolkitBundleDescription,
buildPersistentToolkitBundleLabel,
buildPersistentToolkitInstallLabel,
buildPersistentToolkitNotes,
buildRemoveArgs,
getHostCommandCandidates,
getRuntimeCommandCandidates,
getSupportedPersistentServers,
quoteShellArg,
type McpToolkitBundle,
type McpToolkitInstallItemResult,
type McpToolkitInstallResult,
type McpToolkitPersistentTarget,
} from "./mcp-toolkit-installer-support.js";
export type McpToolkitPersistentTarget = "claude-user" | "codex-global";
export type {
McpToolkitBundle,
McpToolkitInstallItemResult,
McpToolkitInstallResult,
McpToolkitPersistentTarget,
} from "./mcp-toolkit-installer-support.js";
export interface McpToolkitBundle {
readonly label: string;
readonly language: "bash";
readonly value: string;
readonly description: string;
}
export interface McpToolkitInstallItemResult {
readonly id: string;
readonly name: string;
readonly status: "installed" | "skipped";
readonly detail: string;
}
export interface McpToolkitInstallResult {
readonly target: McpToolkitPersistentTarget;
readonly label: string;
readonly installed: readonly McpToolkitInstallItemResult[];
readonly skipped: readonly McpToolkitInstallItemResult[];
readonly notes: readonly string[];
}
interface McpToolkitServerDefinition {
readonly id: string;
readonly name: string;
readonly description: string;
readonly command: readonly string[];
readonly requiredRuntime: "npx" | "uvx";
readonly note?: string;
}
interface CommandResult {
readonly ok: boolean;
readonly exitCode: number | null;
readonly stdout: string;
readonly stderr: string;
readonly error?: string;
}
const commandTimeoutMs = 20_000;
const maxOutputChars = 6_000;
export function buildPersistentToolkitBundle(target: McpToolkitPersistentTarget): McpToolkitBundle {
const label = target === "claude-user"
? "Claude Code user-scope install bundle"
: "Codex CLI global install bundle";
const description = target === "claude-user"
? "Run once to install the supported persistent baseline into Claude Code at user scope."
: "Run once to install the supported persistent baseline into Codex CLI globally.";
const value = buildInstallCommands(target)
export function buildPersistentToolkitBundle(
target: McpToolkitPersistentTarget,
): McpToolkitBundle {
const homeDir = app.getPath("home");
const hostCommand =
target === "claude-user" ? getPreferredClaudeCommand() : "codex";
const value = buildInstallCommands(
target,
hostCommand,
getSupportedPersistentServers(homeDir),
)
.map((command) => command.map(quoteShellArg).join(" "))
.join("\n");
return {
label,
label: buildPersistentToolkitBundleLabel(target),
language: "bash",
value,
description,
description: buildPersistentToolkitBundleDescription(target),
};
}
export async function installPersistentToolkit(target: McpToolkitPersistentTarget): Promise<McpToolkitInstallResult> {
const label = target === "claude-user" ? "Claude Code user scope" : "Codex CLI global";
export async function installPersistentToolkit(
target: McpToolkitPersistentTarget,
): Promise<McpToolkitInstallResult> {
const homeDir = app.getPath("home");
const notes = [
`Filesystem access is scoped to ${formatUserPath(homeDir)} by default.`,
"Git, GitHub, databases, Docker, shell, SSH, and reverse-engineering lanes still stay manual because they need repo paths, auth, or stronger trust decisions.",
];
const hostCommand = target === "claude-user" ? getPreferredClaudeCommand() : "codex";
const hostCheck = await runCommandWithCandidates(getHostCommandCandidates(target));
const hostCommand =
target === "claude-user" ? getPreferredClaudeCommand() : "codex";
const preferredClaudeCommand = getPreferredClaudeCommand();
const hostCheck = await runCommandWithCandidates(
getHostCommandCandidates(target, preferredClaudeCommand),
);
if (!hostCheck.ok) {
throw new Error(`${target === "claude-user" ? "Claude Code" : "Codex CLI"} was not found. Install it first or use Manual Setup. ${summarizeCommandFailure(hostCheck)}`);
throw new Error(
`${target === "claude-user" ? "Claude Code" : "Codex CLI"} was not found. Install it first or use Manual Setup. ${summarizeCommandFailure(hostCheck)}`,
);
}
const npxCheck = await runCommandWithCandidates(getRuntimeCommandCandidates("npx"));
const npxCheck = await runCommandWithCandidates(
getRuntimeCommandCandidates("npx"),
);
if (!npxCheck.ok) {
throw new Error(`Node.js and npx are required for the supported persistent baseline. Install Node.js first or use Manual Setup. ${summarizeCommandFailure(npxCheck)}`);
throw new Error(
`Node.js and npx are required for the supported persistent baseline. Install Node.js first or use Manual Setup. ${summarizeCommandFailure(npxCheck)}`,
);
}
const uvxCheck = await runCommandWithCandidates(getRuntimeCommandCandidates("uvx"));
const installableServers = getSupportedPersistentServers();
const uvxCheck = await runCommandWithCandidates(
getRuntimeCommandCandidates("uvx"),
);
const installableServers = getSupportedPersistentServers(homeDir);
const results: McpToolkitInstallItemResult[] = [];
for (const server of installableServers) {
@ -102,7 +94,7 @@ export async function installPersistentToolkit(target: McpToolkitPersistentTarge
continue;
}
await removeManagedServer(target, server.id);
await runCommand(hostCommand, buildRemoveArgs(target, server.id));
const addResult = await runCommand(hostCommand, buildAddArgs(target, server));
if (!addResult.ok) {
results.push({
@ -124,272 +116,13 @@ export async function installPersistentToolkit(target: McpToolkitPersistentTarge
return {
target,
label,
label: buildPersistentToolkitInstallLabel(target),
installed: results.filter((entry) => entry.status === "installed"),
skipped: results.filter((entry) => entry.status === "skipped"),
notes: uvxCheck.ok
? notes
: [...notes, "Browser Use and Fetch / Web were skipped because `uvx` is not installed on this machine."],
notes: buildPersistentToolkitNotes(homeDir, uvxCheck.ok),
};
}
function buildInstallCommands(target: McpToolkitPersistentTarget): readonly (readonly string[])[] {
return getSupportedPersistentServers().map((server) => {
const command = target === "claude-user" ? getPreferredClaudeCommand() : "codex";
return [command, ...buildAddArgs(target, server)];
});
}
function getSupportedPersistentServers(): readonly McpToolkitServerDefinition[] {
const homeDir = app.getPath("home");
return [
{
id: "familiaros-filesystem",
name: "Filesystem",
description: "Persistent filesystem access scoped to your home directory.",
command: ["npx", "-y", "@modelcontextprotocol/server-filesystem", homeDir],
requiredRuntime: "npx",
note: "Scoped to your home directory by default. Narrow it later if you want a tighter boundary.",
},
{
id: "familiaros-playwright",
name: "Playwright",
description: "Browser automation and browser validation tools.",
command: ["npx", "@playwright/mcp@latest"],
requiredRuntime: "npx",
},
{
id: "familiaros-memory",
name: "Memory",
description: "Persistent project-memory tools for the host agent.",
command: ["npx", "-y", "@modelcontextprotocol/server-memory"],
requiredRuntime: "npx",
},
{
id: "familiaros-context7",
name: "Context7 / Docs",
description: "Up-to-date library and framework docs lookup.",
command: ["npx", "-y", "@upstash/context7-mcp"],
requiredRuntime: "npx",
},
{
id: "familiaros-fetch",
name: "Fetch / Web",
description: "Lightweight web retrieval without full browser automation.",
command: ["uvx", "mcp-server-fetch"],
requiredRuntime: "uvx",
},
{
id: "familiaros-sequential-thinking",
name: "Sequential Thinking",
description: "Structured planning and branching reasoning tools.",
command: ["npx", "-y", "@modelcontextprotocol/server-sequential-thinking"],
requiredRuntime: "npx",
},
{
id: "familiaros-browser-use",
name: "Browser Use",
description: "Higher-level browser task execution for agentic web work.",
command: ["uvx", "--from", "browser-use[cli]", "browser-use", "--mcp"],
requiredRuntime: "uvx",
note: "The entry is installed, but Browser Use still needs its own runtime credential in the host environment.",
},
];
}
function buildAddArgs(target: McpToolkitPersistentTarget, server: McpToolkitServerDefinition): readonly string[] {
const hostCommand = target === "claude-user";
return hostCommand
? ["mcp", "add", "--scope", "user", server.id, "--", ...server.command]
: ["mcp", "add", server.id, "--", ...server.command];
}
async function removeManagedServer(target: McpToolkitPersistentTarget, serverId: string): Promise<void> {
const command = target === "claude-user" ? getPreferredClaudeCommand() : "codex";
const args = target === "claude-user"
? ["mcp", "remove", "--scope", "user", serverId]
: ["mcp", "remove", serverId];
await runCommand(command, args).catch(() => undefined);
}
function getPreferredClaudeCommand(): string {
return getAppStateSnapshot().preferences.claudeCommandPath || "claude";
}
function getHostCommandCandidates(target: McpToolkitPersistentTarget): readonly string[] {
if (target === "claude-user") {
return getCommandCandidates(getPreferredClaudeCommand(), "claude");
}
return getCommandCandidates("codex", "codex");
}
function getRuntimeCommandCandidates(command: "npx" | "uvx"): readonly string[] {
return getCommandCandidates(command, command);
}
function getCommandCandidates(preferred: string, baseName: string): readonly string[] {
if (preferred !== baseName) {
return preferred.toLowerCase().endsWith(".cmd") ? [preferred] : [preferred, ...(process.platform === "win32" ? [`${preferred}.cmd`] : [])];
}
if (process.platform === "win32") return [baseName, `${baseName}.cmd`];
return [baseName];
}
async function runCommandWithCandidates(candidates: readonly string[]): Promise<CommandResult> {
let lastResult: CommandResult = { ok: false, exitCode: null, stdout: "", stderr: "", error: "Command was not found." };
for (const candidate of candidates) {
const result = await runCommand(candidate, ["--version"]);
if (result.ok || !looksLikeMissingCommand(result)) {
return result;
}
lastResult = result;
}
return lastResult;
}
function runCommand(command: string, args: readonly string[]): Promise<CommandResult> {
return new Promise((resolve) => {
const shellCommand = process.platform === "win32" && command.toLowerCase().endsWith(".cmd") ? "cmd.exe" : command;
const shellArgs = process.platform === "win32" && command.toLowerCase().endsWith(".cmd")
? ["/d", "/s", "/c", command, ...args]
: [...args];
let child;
try {
child = spawn(shellCommand, shellArgs, {
cwd: app.getPath("home"),
env: createCommandEnv(),
windowsHide: true,
shell: false,
});
} catch (error) {
resolve({
ok: false,
exitCode: null,
stdout: "",
stderr: "",
error: error instanceof Error ? error.message : "Command failed to start.",
});
return;
}
let stdout = "";
let stderr = "";
let settled = false;
const timer = setTimeout(() => {
if (settled) return;
settled = true;
child.kill();
resolve({
ok: false,
exitCode: null,
stdout: truncateOutput(stdout),
stderr: truncateOutput(stderr),
error: "Command timed out.",
});
}, commandTimeoutMs);
child.stdout?.on("data", (chunk: Buffer) => {
stdout = truncateOutput(stdout + chunk.toString("utf8"));
});
child.stderr?.on("data", (chunk: Buffer) => {
stderr = truncateOutput(stderr + chunk.toString("utf8"));
});
child.on("error", (error) => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve({
ok: false,
exitCode: null,
stdout: truncateOutput(stdout),
stderr: truncateOutput(stderr),
error: error.message,
});
});
child.on("close", (code) => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve({
ok: code === 0,
exitCode: code,
stdout: truncateOutput(stdout),
stderr: truncateOutput(stderr),
});
});
});
}
function createCommandEnv(): NodeJS.ProcessEnv {
const separator = process.platform === "win32" ? ";" : ":";
const existingPath = process.env.PATH ?? "";
return {
...process.env,
PATH: dedupePathEntries([existingPath, ...getExtraCommandPaths()], separator).join(separator),
};
}
function getExtraCommandPaths(): readonly string[] {
if (process.platform === "win32") return [];
const home = app.getPath("home");
const env = process.env;
return filterExistingPaths([
"/opt/homebrew/bin",
"/opt/homebrew/sbin",
"/usr/local/bin",
"/usr/local/sbin",
"/usr/bin",
"/bin",
"/usr/sbin",
"/sbin",
join(home, "bin"),
join(home, ".local", "bin"),
join(home, ".opencode", "bin"),
join(env.VOLTA_HOME || join(home, ".volta"), "bin"),
join(env.BUN_INSTALL || join(home, ".bun"), "bin"),
join(env.MISE_DATA_DIR || join(home, ".local", "share", "mise"), "shims"),
join(env.ASDF_DATA_DIR || join(home, ".asdf"), "shims"),
env.PNPM_HOME,
join(home, ".local", "share", "pnpm"),
join(home, "Library", "pnpm"),
join(env.NVM_DIR || join(home, ".nvm"), "current", "bin"),
]);
}
function filterExistingPaths(paths: readonly (string | undefined)[]): readonly string[] {
return paths.filter((path): path is string => Boolean(path && existsSync(path)));
}
function dedupePathEntries(paths: readonly string[], separator: string): readonly string[] {
const seen = new Set<string>();
const entries: string[] = [];
for (const path of paths.flatMap((value) => value.split(separator)).filter(Boolean)) {
if (seen.has(path)) continue;
seen.add(path);
entries.push(path);
}
return entries;
}
function looksLikeMissingCommand(result: CommandResult): boolean {
return Boolean(result.error && /ENOENT|not found/i.test(result.error));
}
function summarizeCommandFailure(result: CommandResult): string {
const detail = result.stderr || result.stdout || result.error || `exit code ${result.exitCode ?? "unknown"}`;
return truncateOutput(detail).trim() || "command failed.";
}
function truncateOutput(value: string): string {
return value.length > maxOutputChars ? value.slice(value.length - maxOutputChars) : value;
}
function formatUserPath(path: string): string {
return path.replace(app.getPath("home"), "~");
}
function quoteShellArg(value: string): string {
if (/^[A-Za-z0-9_./:@-]+$/.test(value)) return value;
return JSON.stringify(value);
}

View file

@ -11,6 +11,7 @@ const controlCenterIpcPetSource = readFileSync(resolve(desktopRoot, "src/control
const controlCenterIpcPluginSource = readFileSync(resolve(desktopRoot, "src/control-center-ipc-plugin.ts"), "utf8");
const controlCenterIpcSettingsSource = readFileSync(resolve(desktopRoot, "src/control-center-ipc-settings.ts"), "utf8");
const agentBarrelSource = readFileSync(resolve(desktopRoot, "src/control-center-agent-services.ts"), "utf8");
const toolkitInstallerSource = readFileSync(resolve(desktopRoot, "src/mcp-toolkit-installer.ts"), "utf8");
const coreBarrelSource = readFileSync(resolve(desktopRoot, "src/control-center-core-services.ts"), "utf8");
const dataBarrelSource = readFileSync(resolve(desktopRoot, "src/control-center-data-services.ts"), "utf8");
const petBarrelSource = readFileSync(resolve(desktopRoot, "src/control-center-pet-services.ts"), "utf8");
@ -31,6 +32,8 @@ assert.match(controlCenterIpcCombinedSource, /from "\.\/control-center-pet-servi
assert.match(agentBarrelSource, /buildFamiliarOSMcpServerPreview/);
assert.match(agentBarrelSource, /refreshAgentPetContent/);
assert.match(agentBarrelSource, /installPersistentToolkit/);
assert.match(toolkitInstallerSource, /from "\.\/mcp-toolkit-installer-support(?:\.js)?"/, "MCP toolkit installer must import the extracted support seam.");
assert.match(toolkitInstallerSource, /from "\.\/mcp-toolkit-installer-commands(?:\.js)?"/, "MCP toolkit installer must import the extracted command seam.");
assert.match(coreBarrelSource, /getAppStateSnapshot/);
assert.match(coreBarrelSource, /getSettingsStateSnapshot/);
assert.match(coreBarrelSource, /openUpdateReleasePage/);

View file

@ -0,0 +1,58 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const desktopRoot =
process.env.FAMILIAROS_DESKTOP_ROOT ??
resolve(dirname(fileURLToPath(import.meta.url)), "..");
const installerSource = readFileSync(
resolve(desktopRoot, "src/mcp-toolkit-installer.ts"),
"utf8",
);
const installerSupportSource = readFileSync(
resolve(desktopRoot, "src/mcp-toolkit-installer-support.ts"),
"utf8",
);
const installerCommandsSource = readFileSync(
resolve(desktopRoot, "src/mcp-toolkit-installer-commands.ts"),
"utf8",
);
assert.match(
installerSource,
/from "\.\/mcp-toolkit-installer-support(?:\.js)?"/,
"MCP toolkit installer must import the extracted support seam.",
);
assert.match(
installerSource,
/from "\.\/mcp-toolkit-installer-commands(?:\.js)?"/,
"MCP toolkit installer must import the extracted command seam.",
);
assert.match(
installerSource,
/export async function installPersistentToolkit/,
"MCP toolkit installer must keep exporting the public install flow.",
);
assert.match(
installerSupportSource,
/export function getSupportedPersistentServers/,
"MCP toolkit installer support seam must export supported server definitions.",
);
assert.match(
installerSupportSource,
/export function buildInstallCommands/,
"MCP toolkit installer support seam must export bundle command shaping.",
);
assert.match(
installerCommandsSource,
/export function runCommand/,
"MCP toolkit installer command seam must export process execution helpers.",
);
assert.match(
installerCommandsSource,
/export async function runCommandWithCandidates/,
"MCP toolkit installer command seam must export command discovery helpers.",
);
console.error("MCP toolkit installer seam validation passed.");