import type { McpScope, McpTarget } from "../cli/commands/mcp/paths"; import type { SkillScope, SkillTarget } from "../cli/commands/skill/paths"; import type { ConnectorVerifierOptions, ConnectorVerificationTarget, } from "../core/connector-verifier"; import type { ActivationVerificationReceipt, StorePort, StoreResult, } from "../store/types"; import { installMcpToTarget } from "../cli/commands/mcp/install"; import { buildMcpServerEntry, getTargetDisplayName, } from "../cli/commands/mcp/paths"; import { checkMcpTargetStatus, toMcpConnectorVerificationTarget, } from "../cli/commands/mcp/status"; import { installSkillToTarget } from "../cli/commands/skill/install"; import { resolveSkillPaths } from "../cli/commands/skill/paths"; import { verifyConnectorActivation } from "../core/connector-verifier"; export interface ConnectorStatus { id: string; appName: string; installKind: "skill" | "mcp"; target: string; scope: "user" | "project"; installed: boolean; path: string; summary: string; nextAction: string; mode: { label: string; detail: string; }; error?: string; } interface SkillConnectorDefinition { id: string; appName: string; installKind: "skill"; target: SkillTarget; scope: SkillScope; mode: ConnectorStatus["mode"]; } interface McpConnectorDefinition { id: string; appName: string; installKind: "mcp"; target: McpTarget; scope: McpScope; mode: ConnectorStatus["mode"]; } type ConnectorDefinition = SkillConnectorDefinition | McpConnectorDefinition; export interface ConnectorDefinitionSummary { id: string; kind: "skill" | "mcp"; target: string; scope: "user" | "project"; } interface SkillInspection { installed: boolean; path: string; unavailable: boolean; } interface ConnectorInstallContext { cwd?: string; homeDir?: string; indexName?: string; configPath?: string; } const SKILL_PATH_UNAVAILABLE_ERROR = "Skill path configuration is invalid or unavailable."; function unresolvedSkillPath(target: SkillTarget): string { return `unresolved-skill-path/${target}`; } async function inspectSkillConnector( definition: SkillConnectorDefinition, overrides?: { cwd?: string; homeDir?: string } ): Promise { try { const paths = resolveSkillPaths({ scope: definition.scope, target: definition.target, ...overrides, }); return { installed: await Bun.file(`${paths.gnoDir}/SKILL.md`).exists(), path: paths.gnoDir, unavailable: false, }; } catch { return { installed: false, path: unresolvedSkillPath(definition.target), unavailable: true, }; } } function toSkillVerificationTarget( definition: SkillConnectorDefinition, inspection: SkillInspection ): ConnectorVerificationTarget { if (inspection.unavailable) { return { kind: "skill", id: definition.id, target: definition.target, scope: definition.scope, configPath: inspection.path, installed: false, configError: true, }; } return { kind: "skill", id: definition.id, target: definition.target, scope: definition.scope, configPath: inspection.path, installed: inspection.installed, }; } const CONNECTOR_DEFINITIONS = [ { id: "claude-code-skill", appName: "Claude Code", installKind: "skill", target: "claude", scope: "user", mode: { label: "Read/search via skill", detail: "Recommended default. The agent can search and retrieve with GNO without editing client JSON.", }, }, { id: "claude-desktop-mcp", appName: "Claude Desktop", installKind: "mcp", target: "claude-desktop", scope: "user", mode: { label: "Read/search via MCP", detail: "Recommended default. Write-capable MCP can stay an advanced CLI step.", }, }, { id: "cursor-mcp", appName: "Cursor", installKind: "mcp", target: "cursor", scope: "user", mode: { label: "Read/search via MCP", detail: "Recommended default for editor-side agent access.", }, }, { id: "codex-skill", appName: "Codex", installKind: "skill", target: "codex", scope: "user", mode: { label: "Read/search via skill", detail: "Fastest setup for Codex CLI. MCP remains available separately if needed later.", }, }, { id: "opencode-skill", appName: "OpenCode", installKind: "skill", target: "opencode", scope: "user", mode: { label: "Read/search via skill", detail: "Recommended default. Uses the existing skill install path.", }, }, { id: "openclaw-skill", appName: "OpenClaw", installKind: "skill", target: "openclaw", scope: "user", mode: { label: "Read/search via skill", detail: "Recommended default for local agent access without manual file edits.", }, }, { id: "hermes-skill", appName: "Hermes Agent", installKind: "skill", target: "hermes", scope: "user", mode: { label: "Read/search via skill", detail: "Recommended default for Hermes Agent. Uses the standard ~/.hermes/skills path.", }, }, ] as const satisfies readonly ConnectorDefinition[]; export type ConnectorId = (typeof CONNECTOR_DEFINITIONS)[number]["id"]; export function getConnectorDefinition( id: string ): ConnectorDefinitionSummary | null { const definition = CONNECTOR_DEFINITIONS.find((entry) => entry.id === id); return definition ? { id: definition.id, kind: definition.installKind, target: definition.target, scope: definition.scope, } : null; } export function isConnectorId(id: string): id is ConnectorId { return getConnectorDefinition(id) !== null; } export async function getConnectorStatuses(overrides?: { cwd?: string; homeDir?: string; }): Promise { const statuses = await Promise.all( CONNECTOR_DEFINITIONS.map(async (definition) => { if (definition.installKind === "skill") { const inspection = await inspectSkillConnector(definition, overrides); return { id: definition.id, appName: definition.appName, installKind: definition.installKind, target: definition.target, scope: definition.scope, installed: inspection.installed, path: inspection.path, summary: inspection.unavailable ? `${definition.appName} skill path is unavailable.` : inspection.installed ? `${definition.appName} skill is installed.` : `${definition.appName} skill is not installed yet.`, nextAction: inspection.unavailable ? "Fix the skill path configuration, then reload status." : inspection.installed ? "Restart the agent to reload the skill." : "Install the skill from the app.", mode: definition.mode, ...(inspection.unavailable ? { error: SKILL_PATH_UNAVAILABLE_ERROR } : {}), } satisfies ConnectorStatus; } const status = await checkMcpTargetStatus( definition.target, definition.scope, overrides ?? {} ); return { id: definition.id, appName: definition.appName, installKind: definition.installKind, target: definition.target, scope: definition.scope, installed: status.configured, path: status.configPath, summary: status.configured ? `${definition.appName} MCP is configured.` : `${definition.appName} MCP is not configured yet.`, nextAction: status.configured ? `Restart ${definition.appName} to reload the server.` : "Install the MCP connector from the app.", mode: definition.mode, error: status.error, } satisfies ConnectorStatus; }) ); return statuses; } /** Inspect current connector configs without starting any connector runtime. */ export async function getConnectorVerificationTargets(overrides?: { cwd?: string; homeDir?: string; }): Promise { return Promise.all( CONNECTOR_DEFINITIONS.map(async (definition) => { if (definition.installKind === "skill") { const inspection = await inspectSkillConnector(definition, overrides); return toSkillVerificationTarget(definition, inspection); } const status = await checkMcpTargetStatus( definition.target, definition.scope, overrides ?? {} ); return toMcpConnectorVerificationTarget(definition.id, status); }) ); } export async function installConnector( id: string, options?: { reinstall?: boolean; }, overrides?: ConnectorInstallContext ): Promise { const definition = CONNECTOR_DEFINITIONS.find((entry) => entry.id === id); if (!definition) { throw new Error(`Unknown connector: ${id}`); } const currentStatuses = await getConnectorStatuses(overrides); const currentStatus = currentStatuses.find((entry) => entry.id === id); if (currentStatus?.installed && !options?.reinstall) { throw new Error( `${currentStatus.appName} is already installed. Reinstall explicitly to overwrite the existing GNO entry.` ); } if (definition.installKind === "skill") { await installSkillToTarget( definition.scope, definition.target, options?.reinstall ?? false, overrides ); } else { const targetOverrides = overrides ? { cwd: overrides.cwd, homeDir: overrides.homeDir } : undefined; await installMcpToTarget( definition.target, definition.scope, buildMcpServerEntry({ enableWrite: false, indexName: overrides?.indexName, configPath: overrides?.configPath, }), { force: options?.reinstall ?? false, dryRun: false, ...targetOverrides, } ); } const [status] = await getConnectorStatuses(overrides).then((all) => all.filter((entry) => entry.id === id) ); if (!status) { throw new Error(`Failed to reload connector: ${id}`); } return status; } export function getConnectorDisplayName(id: string): string { const definition = CONNECTOR_DEFINITIONS.find((entry) => entry.id === id); if (!definition) { return id; } if (definition.installKind === "mcp") { return `${definition.appName} (${getTargetDisplayName(definition.target)})`; } return definition.appName; } /** * Resolve and verify one connector without editing its client configuration. * Kept separate from passive status listing because this starts a local MCP * child and performs a real, collection-scoped retrieval smoke. */ export async function verifyInstalledConnector( id: string, store: StorePort, collection: string, options?: ConnectorVerifierOptions, overrides?: { cwd?: string; homeDir?: string } ): Promise> { const definition = CONNECTOR_DEFINITIONS.find((entry) => entry.id === id); if (!definition) { throw new Error(`Unknown connector: ${id}`); } let target: ConnectorVerificationTarget; if (definition.installKind === "skill") { const inspection = await inspectSkillConnector(definition, overrides); target = toSkillVerificationTarget(definition, inspection); } else { const status = await checkMcpTargetStatus( definition.target, definition.scope, overrides ?? {} ); target = toMcpConnectorVerificationTarget(definition.id, status); } return verifyConnectorActivation(store, collection, target, options); }