Extract Claude MCP action seam

This commit is contained in:
OpenPets Dev 2026-06-19 22:05:55 +00:00
parent 116d312cca
commit b913978a3c
6 changed files with 204 additions and 153 deletions

View file

@ -0,0 +1,181 @@
import { app } from "electron";
import type { ClaudeCommandSpec, ClaudeMcpPreview, FamiliarOSCommandMode } from "@familiaros/claude";
import {
safeInstallClaudeMemory,
safeUninstallClaudeMemory,
summarizeMemoryMessages,
writeActionJournal,
type AgentSetupJournalEntry,
type JournalAction,
} from "./agent-setup-support.js";
import { getPreferredClaudeCommand } from "./agent-setup-command-context.js";
import type { AgentSetupCommandResult } from "./agent-setup-command-runner.js";
import type {
AgentSetupAction,
AgentSetupActionResult,
ClaudeCodeStatus,
} from "./agent-setup.js";
type CommandResult = AgentSetupCommandResult;
type AgentSetupClaudeMcpActionHelpers = {
readonly selectedPetId: string | undefined;
readonly commandMode: FamiliarOSCommandMode;
readonly detectClaudeCodeStatus: (selectedPetId: string | undefined, commandMode: FamiliarOSCommandMode) => Promise<ClaudeCodeStatus>;
readonly formatUserPath: (path: string | undefined) => string | undefined;
readonly sanitizeOutput: (value: string) => string;
readonly summarizeCommandResult: (result: CommandResult) => string;
readonly runClaudeCommand: (spec: ClaudeCommandSpec) => Promise<CommandResult>;
readonly appendAgentSetupJournal: (entry: Omit<AgentSetupJournalEntry, "timestamp"> & { readonly timestamp?: string }) => void;
readonly journalActionFor: (action: AgentSetupAction) => JournalAction;
};
export async function runRemoveOnlyClaudeMcpAction(
action: AgentSetupAction,
commandMode: FamiliarOSCommandMode,
helpers: AgentSetupClaudeMcpActionHelpers,
): Promise<AgentSetupActionResult> {
return runRemove(createRemoveOnlyPreview(commandMode), "Unknown", action, helpers);
}
export async function runClaudeMcpAction(
action: AgentSetupAction,
preview: ClaudeMcpPreview,
helpers: AgentSetupClaudeMcpActionHelpers,
): Promise<AgentSetupActionResult> {
const detection = await helpers.detectClaudeCodeStatus(helpers.selectedPetId, helpers.commandMode);
const previousStatus = detection.label;
if (detection.state === "not_detected") {
const result = {
ok: false,
action,
message: "Claude Code was not found. Install Claude Code or use Copy command to configure manually.",
changed: false,
} satisfies AgentSetupActionResult;
helpers.appendAgentSetupJournal({
action: helpers.journalActionFor(action),
selectedPetId: helpers.selectedPetId,
command: [preview.add.command, ...preview.add.args],
previousStatus,
success: false,
message: result.message,
});
return result;
}
if (action === "configure") {
return runConfigureClaudeMcpAction(detection, preview, previousStatus, helpers);
}
if (!detection.openPetsEntry.present) {
return runAdd(preview, previousStatus, action, helpers);
}
const removed = await runRemove(preview, previousStatus, action, helpers);
if (!removed.ok) return removed;
const added = await runAdd(preview, previousStatus, action, helpers);
if (!added.ok) {
return {
ok: false,
action,
message: `${added.message} The previous familiaros entry was removed; use this command to restore the intended entry: ${preview.displayCommand}`,
changed: true,
};
}
return {
ok: true,
action,
message: `Replaced Claude Code FamiliarOS MCP entry.${summarizeMemoryMessages(removed.message, added.message)}`,
changed: true,
};
}
function runConfigureClaudeMcpAction(
detection: ClaudeCodeStatus,
preview: ClaudeMcpPreview,
previousStatus: string,
helpers: AgentSetupClaudeMcpActionHelpers,
): Promise<AgentSetupActionResult> | AgentSetupActionResult {
if (detection.openPetsEntry.present && detection.openPetsEntry.verified && detection.openPetsEntry.matchesExpected) {
const memoryResult = safeInstallClaudeMemory(app.getPath("home"));
const message = `FamiliarOS MCP is already configured for Claude Code.${memoryResult.ok ? ` ${memoryResult.message}` : ` Claude instructions were not updated: ${memoryResult.message}`}`;
return {
ok: true,
action: "configure",
message,
changed: memoryResult.ok && memoryResult.message.startsWith("Added"),
};
}
if (detection.openPetsEntry.present) {
return {
ok: false,
action: "configure",
message: "Claude already has an familiaros MCP entry. FamiliarOS will keep it as installed; use Replace only if you want to recreate it with the recommended command.",
changed: false,
};
}
return runAdd(preview, previousStatus, "configure", helpers);
}
async function runAdd(
preview: ClaudeMcpPreview,
previousStatus: string,
action: AgentSetupAction,
helpers: AgentSetupClaudeMcpActionHelpers,
): Promise<AgentSetupActionResult> {
const result = await helpers.runClaudeCommand(preview.add);
const memoryResult = result.ok ? safeInstallClaudeMemory(app.getPath("home")) : { ok: false as const, message: "" };
const message = result.ok
? `Configured Claude Code FamiliarOS MCP entry.${memoryResult.ok ? ` ${memoryResult.message}` : ` Claude instructions were not updated: ${memoryResult.message}`}`
: `Claude MCP add failed: ${helpers.summarizeCommandResult(result)}`;
writeActionJournal({
entry: {
action: helpers.journalActionFor(action),
selectedPetId: helpers.selectedPetId,
command: [preview.add.command, ...preview.add.args],
previousStatus,
success: result.ok,
message,
},
userDataPath: app.getPath("userData"),
formatUserPath: helpers.formatUserPath,
sanitizeOutput: helpers.sanitizeOutput,
});
return { ok: result.ok, action, message, changed: result.ok };
}
async function runRemove(
preview: ClaudeMcpPreview,
previousStatus: string,
action: AgentSetupAction,
helpers: AgentSetupClaudeMcpActionHelpers,
): Promise<AgentSetupActionResult> {
const result = await helpers.runClaudeCommand(preview.remove);
const memoryResult = result.ok ? safeUninstallClaudeMemory(app.getPath("home")) : { ok: false as const, message: "" };
const message = result.ok
? `Removed Claude Code FamiliarOS MCP entry.${memoryResult.ok ? ` ${memoryResult.message}` : ` Claude instructions were not updated: ${memoryResult.message}`}`
: `Claude MCP remove failed: ${helpers.summarizeCommandResult(result)}`;
writeActionJournal({
entry: {
action: helpers.journalActionFor(action),
selectedPetId: helpers.selectedPetId,
command: [preview.remove.command, ...preview.remove.args],
previousStatus,
success: result.ok,
message,
},
userDataPath: app.getPath("userData"),
formatUserPath: helpers.formatUserPath,
sanitizeOutput: helpers.sanitizeOutput,
});
return { ok: result.ok, action, message, changed: result.ok };
}
function createRemoveOnlyPreview(commandMode: FamiliarOSCommandMode): ClaudeMcpPreview {
const claude = getPreferredClaudeCommand();
return {
commandMode,
add: { command: claude, args: [] },
remove: { command: claude, args: ["mcp", "remove", "--scope", "user", "familiaros"] },
mcpJson: { mcpServers: { familiaros: { type: "stdio", command: "node", args: [] } } },
displayCommand: "",
};
}

View file

@ -20,17 +20,17 @@ export {
getOpenCodeSetup,
runAgentSetupMcpServerHealthCheck,
} from "./agent-setup-actions-tooling.js";
import {
runClaudeMcpAction,
runRemoveOnlyClaudeMcpAction,
} from "./agent-setup-actions-claude-mcp.js";
import {
createHookJournalCommand,
safeInstallClaudeMemory,
safeUninstallClaudeMemory,
summarizeMemoryMessages,
writeActionJournal,
type AgentSetupJournalEntry,
type JournalAction,
} from "./agent-setup-support.js";
import {
getPreferredClaudeCommand,
getPreferredNodeCommand,
} from "./agent-setup-command-context.js";
import type { AgentSetupCommandResult } from "./agent-setup-command-runner.js";
@ -114,9 +114,7 @@ async function runImmediateAction(
changed: result.ok && result.message.startsWith("Added"),
};
}
if (action === "remove") {
return runRemove(createRemoveOnlyPreview(helpers.commandMode), "Unknown", action, helpers);
}
if (action === "remove") return runRemoveOnlyClaudeMcpAction(action, helpers.commandMode, helpers);
return undefined;
}
@ -180,144 +178,3 @@ function runInstallHooksAction(
});
return { ok: result.status !== "error", action, message, changed: result.changed };
}
async function runClaudeMcpAction(
action: AgentSetupAction,
preview: ClaudeMcpPreview,
helpers: AgentSetupActionHelpers,
): Promise<AgentSetupActionResult> {
const detection = await helpers.detectClaudeCodeStatus(helpers.selectedPetId, helpers.commandMode);
const previousStatus = detection.label;
if (detection.state === "not_detected") {
const result = {
ok: false,
action,
message: "Claude Code was not found. Install Claude Code or use Copy command to configure manually.",
changed: false,
} satisfies AgentSetupActionResult;
helpers.appendAgentSetupJournal({
action: helpers.journalActionFor(action),
selectedPetId: helpers.selectedPetId,
command: [preview.add.command, ...preview.add.args],
previousStatus,
success: false,
message: result.message,
});
return result;
}
if (action === "configure") {
return runConfigureClaudeMcpAction(detection, preview, previousStatus, helpers);
}
if (!detection.openPetsEntry.present) {
return runAdd(preview, previousStatus, action, helpers);
}
const removed = await runRemove(preview, previousStatus, action, helpers);
if (!removed.ok) return removed;
const added = await runAdd(preview, previousStatus, action, helpers);
if (!added.ok) {
return {
ok: false,
action,
message: `${added.message} The previous familiaros entry was removed; use this command to restore the intended entry: ${preview.displayCommand}`,
changed: true,
};
}
return {
ok: true,
action,
message: `Replaced Claude Code FamiliarOS MCP entry.${summarizeMemoryMessages(removed.message, added.message)}`,
changed: true,
};
}
function runConfigureClaudeMcpAction(
detection: ClaudeCodeStatus,
preview: ClaudeMcpPreview,
previousStatus: string,
helpers: AgentSetupActionHelpers,
): Promise<AgentSetupActionResult> | AgentSetupActionResult {
if (detection.openPetsEntry.present && detection.openPetsEntry.verified && detection.openPetsEntry.matchesExpected) {
const memoryResult = safeInstallClaudeMemory(app.getPath("home"));
const message = `FamiliarOS MCP is already configured for Claude Code.${memoryResult.ok ? ` ${memoryResult.message}` : ` Claude instructions were not updated: ${memoryResult.message}`}`;
return {
ok: true,
action: "configure",
message,
changed: memoryResult.ok && memoryResult.message.startsWith("Added"),
};
}
if (detection.openPetsEntry.present) {
return {
ok: false,
action: "configure",
message: "Claude already has an familiaros MCP entry. FamiliarOS will keep it as installed; use Replace only if you want to recreate it with the recommended command.",
changed: false,
};
}
return runAdd(preview, previousStatus, "configure", helpers);
}
async function runAdd(
preview: ClaudeMcpPreview,
previousStatus: string,
action: AgentSetupAction,
helpers: AgentSetupActionHelpers,
): Promise<AgentSetupActionResult> {
const result = await helpers.runClaudeCommand(preview.add);
const memoryResult = result.ok ? safeInstallClaudeMemory(app.getPath("home")) : { ok: false as const, message: "" };
const message = result.ok
? `Configured Claude Code FamiliarOS MCP entry.${memoryResult.ok ? ` ${memoryResult.message}` : ` Claude instructions were not updated: ${memoryResult.message}`}`
: `Claude MCP add failed: ${helpers.summarizeCommandResult(result)}`;
writeActionJournal({
entry: {
action: helpers.journalActionFor(action),
selectedPetId: helpers.selectedPetId,
command: [preview.add.command, ...preview.add.args],
previousStatus,
success: result.ok,
message,
},
userDataPath: app.getPath("userData"),
formatUserPath: helpers.formatUserPath,
sanitizeOutput: helpers.sanitizeOutput,
});
return { ok: result.ok, action, message, changed: result.ok };
}
async function runRemove(
preview: ClaudeMcpPreview,
previousStatus: string,
action: AgentSetupAction,
helpers: AgentSetupActionHelpers,
): Promise<AgentSetupActionResult> {
const result = await helpers.runClaudeCommand(preview.remove);
const memoryResult = result.ok ? safeUninstallClaudeMemory(app.getPath("home")) : { ok: false as const, message: "" };
const message = result.ok
? `Removed Claude Code FamiliarOS MCP entry.${memoryResult.ok ? ` ${memoryResult.message}` : ` Claude instructions were not updated: ${memoryResult.message}`}`
: `Claude MCP remove failed: ${helpers.summarizeCommandResult(result)}`;
writeActionJournal({
entry: {
action: helpers.journalActionFor(action),
selectedPetId: helpers.selectedPetId,
command: [preview.remove.command, ...preview.remove.args],
previousStatus,
success: result.ok,
message,
},
userDataPath: app.getPath("userData"),
formatUserPath: helpers.formatUserPath,
sanitizeOutput: helpers.sanitizeOutput,
});
return { ok: result.ok, action, message, changed: result.ok };
}
function createRemoveOnlyPreview(commandMode: FamiliarOSCommandMode): ClaudeMcpPreview {
const claude = getPreferredClaudeCommand();
return {
commandMode,
add: { command: claude, args: [] },
remove: { command: claude, args: ["mcp", "remove", "--scope", "user", "familiaros"] },
mcpJson: { mcpServers: { familiaros: { type: "stdio", command: "node", args: [] } } },
displayCommand: "",
};
}

View file

@ -199,6 +199,7 @@ const appStateStorageSource = readFileSync(join(appDir, "src", "app-state-storag
const agentSetupSource = readFileSync(join(appDir, "src", "agent-setup.ts"), "utf8");
const agentSetupClaudeStatusSource = readFileSync(join(appDir, "src", "agent-setup-claude-status.ts"), "utf8");
const agentSetupActionsSource = readFileSync(join(appDir, "src", "agent-setup-actions.ts"), "utf8");
const agentSetupActionsClaudeMcpSource = readFileSync(join(appDir, "src", "agent-setup-actions-claude-mcp.ts"), "utf8");
const agentSetupActionsToolingSource = readFileSync(join(appDir, "src", "agent-setup-actions-tooling.ts"), "utf8");
const agentSetupCommandRunnerSource = readFileSync(join(appDir, "src", "agent-setup-command-runner.ts"), "utf8");
const agentSetupCommandContextSource = readFileSync(join(appDir, "src", "agent-setup-command-context.ts"), "utf8");
@ -580,6 +581,9 @@ assert.match(agentSetupCommandRunnerSource, /export function createAgentSetupCom
assert.match(agentSetupCommandRunnerSource, /export function summarizeAgentSetupCommandResult/, "agent-setup command-runner seam must export command result summarization.");
assert.match(agentSetupActionsSource, /export async function executeAgentSetupResolvedAction/, "agent-setup action seam must export action execution.");
assert.match(agentSetupActionsSource, /from "\.\/agent-setup-actions-tooling(?:\.js)?"/, "agent-setup action seam must import the extracted tooling seam.");
assert.match(agentSetupActionsSource, /from "\.\/agent-setup-actions-claude-mcp(?:\.js)?"/, "agent-setup action seam must import the extracted Claude MCP action seam.");
assert.match(agentSetupActionsClaudeMcpSource, /export async function runClaudeMcpAction/, "agent-setup Claude MCP seam must export add/configure/replace orchestration.");
assert.match(agentSetupActionsClaudeMcpSource, /export async function runRemoveOnlyClaudeMcpAction/, "agent-setup Claude MCP seam must export the remove-only orchestration.");
assert.match(agentSetupClaudeStatusSource, /export function sanitizeAgentSetupOutput/, "agent-setup Claude status seam must export output sanitization.");
assert.match(agentSetupClaudeStatusSource, /export function safeBuildClaudeMcpPreview/, "agent-setup Claude status seam must export preview shaping.");
assert.match(agentSetupClaudeStatusSource, /export function safeDoctorClaudeHooks/, "agent-setup Claude status seam must export hook doctor wrappers.");
@ -850,7 +854,7 @@ assert.match(agentSetupActionsToolingSource, /from "\.\/agent-setup-editor-tools
assert.match(agentSetupSource, /agent-setup-support\.js/, "Agent setup must import and re-export the extracted support helper seam.");
assert.match(agentSetupActionsToolingSource, /getCliPackageVersion/, "Agent setup tooling seam must compose package version metadata through the command context helper.");
assert.match(agentSetupActionsToolingSource, /buildOpenCodeSetupSnapshot/, "Agent setup tooling seam must delegate OpenCode previews through the editor setup helper.");
assert.match(agentSetupActionsSource, /writeActionJournal/, "Agent setup action seam must delegate journal writes through the support helper.");
assert.match(agentSetupActionsClaudeMcpSource, /writeActionJournal/, "Agent setup Claude MCP seam must delegate journal writes through the support helper.");
assert.match(petWindowRenderSource, /from "\.\/familiar-window-render-bubbles(?:\.js)?"/, "familiar-window-render must compose the extracted bubble helper seam.");
assert.match(petWindowRenderBubblesSource, /export function createBubbleMarkup/, "familiar-window render bubble seam must export the shared bubble markup builder.");
assert.match(petWindowRenderBubblesSource, /export function getBubbleClassName/, "familiar-window render bubble seam must export bubble length classification.");

View file

@ -74,7 +74,8 @@ windows.ts (IPC handlers)
│ ├── Claude MCP preview/status detection and packaged-resource fallback shaping
│ └── Claude/OpenCode command wrappers, output sanitization, and user-path formatting
├── agent-setup-actions.ts
│ ├── runAgentSetupResolvedAction() (configure/replace/remove, install-memory, hook actions)
│ ├── runAgentSetupResolvedAction() shell and immediate action routing
│ ├── agent-setup-actions-claude-mcp.ts (Claude MCP add/remove/configure/replace orchestration)
│ └── agent-setup-actions-tooling.ts (OpenCode/Cursor setup snapshots, global config actions, and FamiliarOS MCP server health)
├── agent-setup-command-context.ts
│ ├── persisted command path preferences
@ -218,6 +219,7 @@ plugin-service-local-support.ts → plugin-local-loader.ts validates selected fo
- `control-center-ipc-pets.ts`: Familiar/catalog/Codex import and default familiar IPC routes for the Control Center
- `control-center-ipc-agent.ts`: Agent setup, toolkit, and FamiliarOS MCP preview/test IPC routes for the Control Center
- `agent-setup-actions.ts`: Extracted action execution shell for hook handling plus Claude MCP add/remove orchestration
- `agent-setup-actions-claude-mcp.ts`: Extracted Claude MCP add/remove/configure/replace orchestration plus memory-aware journaling
- `agent-setup-actions-tooling.ts`: Extracted OpenCode/Cursor setup loading, global config actions, and FamiliarOS MCP server health checks
- `agent-setup-editor-tools-opencode.ts`: Extracted OpenCode global setup status, previews, and config write helpers
- `agent-setup-editor-tools-cursor.ts`: Extracted Cursor global setup status, previews, and MCP config write helpers
@ -350,7 +352,8 @@ plugin-service-local-support.ts → plugin-local-loader.ts validates selected fo
**Agent Integration**:
- `agent-setup.ts`: Agent-setup public entry points, snapshot assembly, selected-pet validation, and action journal coordination
- `agent-setup-actions.ts`: Extracted action execution shell plus Claude MCP add/remove orchestration
- `agent-setup-actions.ts`: Extracted action execution shell plus immediate editor, hook, and memory action routing
- `agent-setup-actions-claude-mcp.ts`: Extracted Claude MCP add/remove/configure/replace orchestration plus memory-aware journaling
- `agent-setup-actions-tooling.ts`: Extracted editor setup loading, global config actions, and FamiliarOS MCP server health checks
- `agent-setup-claude-status.ts`: Extracted Claude MCP preview/status detection, command wrappers, output sanitization, and packaged-resource error shaping
- `agent-setup-command-context.ts`: Agent setup command path persistence, preferred command resolution, and workspace package metadata helpers

View file

@ -6,15 +6,19 @@ import { fileURLToPath } from "node:url";
const desktopRoot = process.env.FAMILIAROS_DESKTOP_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..");
const agentSetupSource = readFileSync(resolve(desktopRoot, "src/agent-setup.ts"), "utf8");
const agentSetupActionsSource = readFileSync(resolve(desktopRoot, "src/agent-setup-actions.ts"), "utf8");
const agentSetupActionsClaudeMcpSource = readFileSync(resolve(desktopRoot, "src/agent-setup-actions-claude-mcp.ts"), "utf8");
const agentSetupActionsToolingSource = readFileSync(resolve(desktopRoot, "src/agent-setup-actions-tooling.ts"), "utf8");
assert.match(agentSetupSource, /from "\.\/agent-setup-actions\.js"/, "Agent setup must import the extracted action seam.");
assert.match(agentSetupActionsSource, /export async function executeAgentSetupResolvedAction/, "Agent setup action seam must export the action executor.");
assert.match(agentSetupActionsSource, /from "\.\/agent-setup-actions-tooling(?:\.js)?"/, "Agent setup action seam must compose the extracted tooling seam.");
assert.match(agentSetupActionsSource, /from "\.\/agent-setup-actions-claude-mcp(?:\.js)?"/, "Agent setup action seam must compose the extracted Claude MCP action seam.");
assert.match(agentSetupActionsToolingSource, /export async function getOpenCodeSetup/, "Agent setup tooling seam must export OpenCode setup loading.");
assert.match(agentSetupActionsToolingSource, /export async function getCursorSetup/, "Agent setup tooling seam must export Cursor setup loading.");
assert.match(agentSetupActionsToolingSource, /export async function runAgentSetupMcpServerHealthCheck/, "Agent setup tooling seam must export MCP server health checks.");
assert.match(agentSetupActionsSource, /from "\.\/agent-setup-support\.js"/, "Agent setup action seam must compose the extracted support helper seam.");
assert.match(agentSetupActionsSource, /from "\.\/agent-setup-command-context\.js"/, "Agent setup action seam must compose the extracted command context seam.");
assert.match(agentSetupActionsClaudeMcpSource, /export async function runClaudeMcpAction/, "Agent setup Claude MCP seam must export Claude MCP action orchestration.");
assert.match(agentSetupActionsClaudeMcpSource, /export async function runRemoveOnlyClaudeMcpAction/, "Agent setup Claude MCP seam must export the remove-only action helper.");
console.error("Agent setup action seam validation passed.");

View file

@ -6,12 +6,14 @@ import { fileURLToPath } from "node:url";
const desktopRoot = process.env.FAMILIAROS_DESKTOP_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..");
const agentSetupSource = readFileSync(resolve(desktopRoot, "src/agent-setup.ts"), "utf8");
const agentSetupActionsSource = readFileSync(resolve(desktopRoot, "src/agent-setup-actions.ts"), "utf8");
const agentSetupActionsClaudeMcpSource = readFileSync(resolve(desktopRoot, "src/agent-setup-actions-claude-mcp.ts"), "utf8");
const agentSetupActionsToolingSource = readFileSync(resolve(desktopRoot, "src/agent-setup-actions-tooling.ts"), "utf8");
const agentSetupSupportSource = readFileSync(resolve(desktopRoot, "src/agent-setup-support.ts"), "utf8");
assert.match(agentSetupSource, /from "\.\/agent-setup-support\.js"/, "Agent setup must import the extracted support helper seam.");
assert.match(agentSetupActionsSource, /safeInstallClaudeMemory\(app\.getPath\("home"\)\)/, "Agent setup action seam must delegate Claude memory installs through the extracted support helper.");
assert.match(agentSetupActionsSource, /writeActionJournal\(\{\s*entry:/, "Agent setup action seam must delegate action journaling through the extracted support helper.");
assert.match(agentSetupActionsSource, /from "\.\/agent-setup-actions-claude-mcp(?:\.js)?"/, "Agent setup action shell must compose the extracted Claude MCP helper seam.");
assert.match(agentSetupActionsSource, /safeInstallClaudeMemory\(app\.getPath\("home"\)\)/, "Agent setup action shell must keep memory installs routed through the support helper.");
assert.match(agentSetupActionsClaudeMcpSource, /writeActionJournal\(\{\s*entry:/, "Agent setup Claude MCP seam must delegate action journaling through the extracted support helper.");
assert.match(agentSetupActionsToolingSource, /buildFamiliarOSMcpServerPreview/, "Agent setup tooling seam must delegate MCP server preview shaping through the extracted support helper.");
assert.match(agentSetupSupportSource, /export function buildFamiliarOSMcpServerPreview/, "Agent setup support helper must own FamiliarOS MCP preview shaping.");