import type { AdapterModelProfileDefinition, AdapterRuntimeCommandSpec, ServerAdapterModule, } from "./types.js"; import { parseAdapterModelsEnv } from "../services/adapter-models-env.js"; import { stampClaudeAgentIdHeader } from "./claude-agent-id-header.js"; import { buildSandboxNpmInstallCommand, getAdapterSessionManagement, } from "@paperclipai/adapter-utils"; import type { AdapterLoginCapability } from "@paperclipai/adapter-utils"; import { execute as claudeExecute, listClaudeSkills, syncClaudeSkills, listClaudeModels, refreshClaudeModels, testEnvironment as claudeTestEnvironment, sessionCodec as claudeSessionCodec, getQuotaWindows as claudeGetQuotaWindows, getConfigSchema as getClaudeConfigSchema, CLAUDE_SETUP_TOKEN_COMMAND, parseSetupTokenPrompt, parseSetupTokenCredential, } from "@paperclipai/adapter-claude-local/server"; import { agentConfigurationDoc as claudeAgentConfigurationDoc, models as claudeModels, modelProfiles as claudeModelProfiles, } from "@paperclipai/adapter-claude-local"; import { execute as codexExecute, listCodexSkills, syncCodexSkills, testEnvironment as codexTestEnvironment, sessionCodec as codexSessionCodec, getQuotaWindows as codexGetQuotaWindows, getConfigSchema as getCodexConfigSchema, CODEX_DEVICE_LOGIN_COMMAND, parseDeviceLoginPrompt, } from "@paperclipai/adapter-codex-local/server"; import { agentConfigurationDoc as codexAgentConfigurationDoc, models as codexModels, modelProfiles as codexModelProfiles, } from "@paperclipai/adapter-codex-local"; import { execute as cursorExecute, listCursorSkills, syncCursorSkills, testEnvironment as cursorTestEnvironment, sessionCodec as cursorSessionCodec, } from "@paperclipai/adapter-cursor-local/server"; import { agentConfigurationDoc as cursorAgentConfigurationDoc, models as cursorModels, modelProfiles as cursorModelProfiles, } from "@paperclipai/adapter-cursor-local"; import { execute as cursorCloudExecute, getConfigSchema as getCursorCloudConfigSchema, sessionCodec as cursorCloudSessionCodec, testEnvironment as cursorCloudTestEnvironment, } from "@paperclipai/adapter-cursor-cloud/server"; import { agentConfigurationDoc as cursorCloudAgentConfigurationDoc } from "@paperclipai/adapter-cursor-cloud"; import { execute as geminiExecute, listGeminiSkills, syncGeminiSkills, testEnvironment as geminiTestEnvironment, sessionCodec as geminiSessionCodec, getConfigSchema as getGeminiConfigSchema, } from "@paperclipai/adapter-gemini-local/server"; import { agentConfigurationDoc as geminiAgentConfigurationDoc, models as geminiModels, modelProfiles as geminiModelProfiles, } from "@paperclipai/adapter-gemini-local"; import { execute as grokExecute, listGrokSkills, syncGrokSkills, testEnvironment as grokTestEnvironment, sessionCodec as grokSessionCodec, GROK_DEVICE_LOGIN_COMMAND, parseGrokDeviceLoginPrompt, } from "@paperclipai/adapter-grok-local/server"; import { agentConfigurationDoc as grokAgentConfigurationDoc, models as grokModels, } from "@paperclipai/adapter-grok-local"; import { execute as kimiExecute, listKimiSkills, syncKimiSkills, testEnvironment as kimiTestEnvironment, sessionCodec as kimiSessionCodec, } from "@paperclipai/adapter-kimi-local/server"; import { agentConfigurationDoc as kimiAgentConfigurationDoc, models as kimiModels, } from "@paperclipai/adapter-kimi-local"; import { createHermesGatewayServerAdapter, createHermesLocalServerAdapter, } from "@paperclipai/hermes-paperclip-adapter"; import { execute as openCodeExecute, listOpenCodeSkills, syncOpenCodeSkills, testEnvironment as openCodeTestEnvironment, sessionCodec as openCodeSessionCodec, listOpenCodeModels, } from "@paperclipai/adapter-opencode-local/server"; import { agentConfigurationDoc as openCodeAgentConfigurationDoc, models as openCodeModels, modelProfiles as openCodeModelProfiles, } from "@paperclipai/adapter-opencode-local"; import { execute as openclawGatewayExecute, testEnvironment as openclawGatewayTestEnvironment, } from "@paperclipai/adapter-openclaw-gateway/server"; import { agentConfigurationDoc as openclawGatewayAgentConfigurationDoc, models as openclawGatewayModels, } from "@paperclipai/adapter-openclaw-gateway"; import { listCodexModels, refreshCodexModels } from "./codex-models.js"; import { listCursorModels } from "./cursor-models.js"; import { execute as piExecute, listPiSkills, syncPiSkills, testEnvironment as piTestEnvironment, sessionCodec as piSessionCodec, listPiModels, } from "@paperclipai/adapter-pi-local/server"; import { agentConfigurationDoc as piAgentConfigurationDoc, modelProfiles as piModelProfiles, } from "@paperclipai/adapter-pi-local"; import { BUILTIN_ADAPTER_TYPES } from "./builtin-adapter-types.js"; import { buildExternalAdapters } from "./plugin-loader.js"; import { getDisabledAdapterTypes } from "../services/adapter-plugin-store.js"; import { processAdapter } from "./process/index.js"; import { httpAdapter } from "./http/index.js"; function readConfiguredCommand(config: Record, fallback: string): string { const value = typeof config.command === "string" ? config.command.trim() : ""; return value.length > 0 ? value : fallback; } function hasPathSeparator(command: string): boolean { return command.includes("/") || command.includes("\\"); } function shellQuote(value: string): string { return `'${value.replace(/'/g, `'"'"'`)}'`; } function buildNpmRuntimeCommandSpec( config: Record, fallbackCommand: string, packageName: string, ): AdapterRuntimeCommandSpec { const command = readConfiguredCommand(config, fallbackCommand); const canSelfInstall = !hasPathSeparator(command) && command === fallbackCommand; const installLine = buildSandboxNpmInstallCommand(packageName); return { command, detectCommand: command, installCommand: canSelfInstall ? `if ! command -v ${shellQuote(command)} >/dev/null 2>&1; then ${installLine}; fi` : null, }; } function buildCursorRuntimeCommandSpec(config: Record): AdapterRuntimeCommandSpec { const command = readConfiguredCommand(config, "agent"); return { command, detectCommand: command, installCommand: null, }; } const retiredAcpxMessage = "The acpx_local adapter has been retired. Existing Claude and Codex ACPX agents should be migrated to claude_local or codex_local with adapterConfig.engine=\"acp\"."; const retiredAcpxAgentConfigurationDoc = `# acpx_local retired Adapter: acpx_local The standalone ACPX adapter has been retired. Use: - claude_local with adapterConfig.engine="acp" for Claude ACP execution. - codex_local with adapterConfig.engine="acp" for Codex ACP execution. Paperclip keeps this tombstone registered so stale acpx_local rows fail clearly instead of falling back to the process adapter. `; // The Claude interactive login capability. Claude runs `claude setup-token` on a // real pseudo-terminal. The user pastes a browser code back into the flow. The // flow uses a fixed host-side timeout and records a stored session identifier on // success. The capability data holds no secret; the callbacks return runtime // values only. const claudeLoginCapability: AdapterLoginCapability = { panelMode: "submitted_browser_code", timeoutPolicy: "fixed", getCommand: () => CLAUDE_SETUP_TOKEN_COMMAND, parsePrompt: (output) => { const prompt = parseSetupTokenPrompt(output); return prompt ? { url: prompt.url } : null; }, captureCredential: (output) => { const token = parseSetupTokenCredential(output); return token === null ? null : Buffer.from(token, "utf8"); }, completionClaim: "storedSessionId", }; // The Codex interactive login capability. Codex runs `codex login --device-auth` // on a real pseudo-terminal, because a pipe emits no login prompt. The flow shows // a one-time code that the user enters in the browser. The caller sets the // host-side timeout. The device-login flow writes its credential inside the // sandbox, so the capability declares no terminal credential capture and no // completion claim. const codexLoginCapability: AdapterLoginCapability = { panelMode: "displayed_code", timeoutPolicy: "caller_bounded", getCommand: () => CODEX_DEVICE_LOGIN_COMMAND, parsePrompt: (output) => { const prompt = parseDeviceLoginPrompt(output); return prompt ? { url: prompt.url, code: prompt.code } : null; }, }; // The Grok interactive login capability. Grok runs `grok login --device-auth` // on a real pseudo-terminal, the same way Codex does. The flow shows a // one-time code that the user enters in the browser. The caller sets the // host-side timeout. The device-login flow writes its credential inside the // sandbox, so the capability declares no terminal credential capture and no // completion claim. `getCommand` is descriptive only: the login path selects // the real command from the closed key map in `login-command.ts`, never from // this member. const grokLoginCapability: AdapterLoginCapability = { panelMode: "displayed_code", timeoutPolicy: "caller_bounded", getCommand: () => GROK_DEVICE_LOGIN_COMMAND, parsePrompt: (output) => { const prompt = parseGrokDeviceLoginPrompt(output); return prompt ? { url: prompt.url, code: prompt.code } : null; }, }; const claudeLocalAdapter: ServerAdapterModule = { type: "claude_local", runtimeToolDelivery: "native_mcp", execute: stampClaudeAgentIdHeader(claudeExecute), testEnvironment: claudeTestEnvironment, acp: { agentId: "claude", skillsMode: "ephemeral", prerequisites: { nodeRange: ">=24.11.0", packages: ["@agentclientprotocol/claude-agent-acp"], }, }, listSkills: listClaudeSkills, syncSkills: syncClaudeSkills, sessionCodec: claudeSessionCodec, sessionManagement: getAdapterSessionManagement("claude_local") ?? undefined, models: claudeModels, modelProfiles: claudeModelProfiles, listModels: listClaudeModels, refreshModels: refreshClaudeModels, supportsLocalAgentJwt: true, supportsInstructionsBundle: true, instructionsPathKey: "instructionsFilePath", requiresMaterializedRuntimeSkills: false, getRuntimeCommandSpec: (config) => buildNpmRuntimeCommandSpec(config, "claude", "@anthropic-ai/claude-code"), agentConfigurationDoc: claudeAgentConfigurationDoc, getConfigSchema: getClaudeConfigSchema, getQuotaWindows: claudeGetQuotaWindows, loginCapability: claudeLoginCapability, }; const acpxLocalAdapter: ServerAdapterModule = { type: "acpx_local", runtimeToolDelivery: "environment", async execute(ctx) { await ctx.onLog("stderr", `${retiredAcpxMessage}\n`); await ctx.onMeta?.({ adapterType: "acpx_local", command: "acpx_local-retired", commandNotes: [retiredAcpxMessage], }); return { exitCode: 1, signal: null, timedOut: false, errorMessage: retiredAcpxMessage, errorCode: "acpx_local_retired", provider: "acpx", summary: retiredAcpxMessage, }; }, async testEnvironment() { return { adapterType: "acpx_local", status: "fail", testedAt: new Date().toISOString(), checks: [ { code: "acpx_local_retired", level: "error", message: retiredAcpxMessage, hint: "Set the agent adapter to claude_local or codex_local and set adapterConfig.engine to acp.", }, ], }; }, models: [], supportsLocalAgentJwt: false, supportsInstructionsBundle: false, requiresMaterializedRuntimeSkills: false, agentConfigurationDoc: retiredAcpxAgentConfigurationDoc, getConfigSchema: () => ({ fields: [] }), }; const codexLocalAdapter: ServerAdapterModule = { type: "codex_local", runtimeToolDelivery: "native_mcp", execute: codexExecute, testEnvironment: codexTestEnvironment, acp: { agentId: "codex", skillsMode: "ephemeral", prerequisites: { nodeRange: ">=24.11.0", packages: ["@agentclientprotocol/codex-acp"], }, }, listSkills: listCodexSkills, syncSkills: syncCodexSkills, sessionCodec: codexSessionCodec, sessionManagement: getAdapterSessionManagement("codex_local") ?? undefined, models: codexModels, modelProfiles: codexModelProfiles, listModels: listCodexModels, refreshModels: refreshCodexModels, supportsLocalAgentJwt: true, supportsInstructionsBundle: true, instructionsPathKey: "instructionsFilePath", requiresMaterializedRuntimeSkills: false, getRuntimeCommandSpec: (config) => buildNpmRuntimeCommandSpec(config, "codex", "@openai/codex"), agentConfigurationDoc: codexAgentConfigurationDoc, getConfigSchema: getCodexConfigSchema, getQuotaWindows: codexGetQuotaWindows, loginCapability: codexLoginCapability, }; const paperclipRunnerAdapter: ServerAdapterModule = { type: "paperclip_runner", runtimeToolDelivery: "environment", async execute(ctx) { const message = "paperclip_runner requires the native runner coordinator"; await ctx.onLog("stderr", `${message}\n`); return { exitCode: 1, signal: null, timedOut: false, errorMessage: message, errorCode: "paperclip_runner_coordinator_required", provider: "codex", summary: message, }; }, async testEnvironment(context) { const result = await codexTestEnvironment(context); return { ...result, adapterType: "paperclip_runner" }; }, listSkills: listCodexSkills, syncSkills: syncCodexSkills, sessionCodec: codexSessionCodec, models: codexModels, listModels: listCodexModels, refreshModels: refreshCodexModels, supportsLocalAgentJwt: false, supportsInstructionsBundle: false, requiresMaterializedRuntimeSkills: false, getRuntimeCommandSpec: (config) => buildNpmRuntimeCommandSpec(config, "codex", "@openai/codex"), agentConfigurationDoc: "# Paperclip Runner\n\nAdapter: paperclip_runner\n\nRuns Codex through the Rust Paperclip runner and authenticated PRP transport.\n", getConfigSchema: () => ({ fields: [ { key: "provider", label: "Provider", type: "select", default: "codex", options: [{ value: "codex", label: "Codex" }], hint: "Paperclip Runner currently supports only Codex app-server.", }, ], }), loginCapability: codexLoginCapability, }; const cursorLocalAdapter: ServerAdapterModule = { type: "cursor", runtimeToolDelivery: "environment", execute: cursorExecute, testEnvironment: cursorTestEnvironment, listSkills: listCursorSkills, syncSkills: syncCursorSkills, sessionCodec: cursorSessionCodec, sessionManagement: getAdapterSessionManagement("cursor") ?? undefined, models: cursorModels, modelProfiles: cursorModelProfiles, listModels: listCursorModels, supportsLocalAgentJwt: true, supportsInstructionsBundle: true, instructionsPathKey: "instructionsFilePath", requiresMaterializedRuntimeSkills: true, getRuntimeCommandSpec: buildCursorRuntimeCommandSpec, agentConfigurationDoc: cursorAgentConfigurationDoc, }; const cursorCloudAdapter: ServerAdapterModule = { type: "cursor_cloud", runtimeToolDelivery: "invocation_context", execute: cursorCloudExecute, testEnvironment: cursorCloudTestEnvironment, sessionCodec: cursorCloudSessionCodec, sessionManagement: getAdapterSessionManagement("cursor_cloud") ?? undefined, models: [], supportsLocalAgentJwt: false, supportsInstructionsBundle: true, instructionsPathKey: "instructionsFilePath", requiresMaterializedRuntimeSkills: false, agentConfigurationDoc: cursorCloudAgentConfigurationDoc, getConfigSchema: getCursorCloudConfigSchema, }; const geminiLocalAdapter: ServerAdapterModule = { type: "gemini_local", runtimeToolDelivery: "environment", execute: geminiExecute, testEnvironment: geminiTestEnvironment, acp: { agentId: "gemini", skillsMode: "ephemeral", prerequisites: { nodeRange: ">=24.11.0", packages: ["@google/gemini-cli"], }, }, listSkills: listGeminiSkills, syncSkills: syncGeminiSkills, sessionCodec: geminiSessionCodec, sessionManagement: getAdapterSessionManagement("gemini_local") ?? undefined, models: geminiModels, modelProfiles: geminiModelProfiles, supportsLocalAgentJwt: true, supportsInstructionsBundle: true, instructionsPathKey: "instructionsFilePath", requiresMaterializedRuntimeSkills: true, getRuntimeCommandSpec: (config) => buildNpmRuntimeCommandSpec(config, "gemini", "@google/gemini-cli"), agentConfigurationDoc: geminiAgentConfigurationDoc, getConfigSchema: getGeminiConfigSchema, }; const grokLocalAdapter: ServerAdapterModule = { type: "grok_local", runtimeToolDelivery: "environment", execute: grokExecute, testEnvironment: grokTestEnvironment, listSkills: listGrokSkills, syncSkills: syncGrokSkills, sessionCodec: grokSessionCodec, sessionManagement: getAdapterSessionManagement("grok_local") ?? undefined, models: grokModels, supportsLocalAgentJwt: true, supportsInstructionsBundle: true, instructionsPathKey: "instructionsFilePath", requiresMaterializedRuntimeSkills: true, getRuntimeCommandSpec: (config) => ({ command: readConfiguredCommand(config, "grok"), detectCommand: readConfiguredCommand(config, "grok"), installCommand: null, }), agentConfigurationDoc: grokAgentConfigurationDoc, loginCapability: grokLoginCapability, }; const kimiLocalAdapter: ServerAdapterModule = { type: "kimi_local", runtimeToolDelivery: "environment", execute: kimiExecute, testEnvironment: kimiTestEnvironment, acp: { agentId: "kimi", skillsMode: "ephemeral", prerequisites: { nodeRange: ">=20.0.0", packages: ["@moonshot-ai/kimi-code"], }, }, listSkills: listKimiSkills, syncSkills: syncKimiSkills, sessionCodec: kimiSessionCodec, sessionManagement: getAdapterSessionManagement("kimi_local") ?? undefined, models: kimiModels, supportsLocalAgentJwt: true, supportsInstructionsBundle: true, instructionsPathKey: "instructionsFilePath", requiresMaterializedRuntimeSkills: true, getRuntimeCommandSpec: (config) => buildNpmRuntimeCommandSpec(config, "kimi", "@moonshot-ai/kimi-code"), agentConfigurationDoc: kimiAgentConfigurationDoc, }; const hermesGatewayAdapter: ServerAdapterModule = { ...createHermesGatewayServerAdapter(), runtimeToolDelivery: "invocation_context", }; const hermesLocalAdapter: ServerAdapterModule = { ...createHermesLocalServerAdapter(), runtimeToolDelivery: "environment", }; const openclawGatewayAdapter: ServerAdapterModule = { type: "openclaw_gateway", runtimeToolDelivery: "invocation_context", execute: openclawGatewayExecute, testEnvironment: openclawGatewayTestEnvironment, models: openclawGatewayModels, supportsLocalAgentJwt: false, supportsInstructionsBundle: false, requiresMaterializedRuntimeSkills: false, agentConfigurationDoc: openclawGatewayAgentConfigurationDoc, }; const openCodeLocalAdapter: ServerAdapterModule = { type: "opencode_local", runtimeToolDelivery: "environment", execute: openCodeExecute, testEnvironment: openCodeTestEnvironment, listSkills: listOpenCodeSkills, syncSkills: syncOpenCodeSkills, sessionCodec: openCodeSessionCodec, models: openCodeModels, modelProfiles: openCodeModelProfiles, sessionManagement: getAdapterSessionManagement("opencode_local") ?? undefined, listModels: listOpenCodeModels, supportsLocalAgentJwt: true, supportsInstructionsBundle: true, instructionsPathKey: "instructionsFilePath", requiresMaterializedRuntimeSkills: true, getRuntimeCommandSpec: (config) => buildNpmRuntimeCommandSpec(config, "opencode", "opencode-ai"), agentConfigurationDoc: openCodeAgentConfigurationDoc, }; const piLocalAdapter: ServerAdapterModule = { type: "pi_local", runtimeToolDelivery: "environment", execute: piExecute, testEnvironment: piTestEnvironment, listSkills: listPiSkills, syncSkills: syncPiSkills, sessionCodec: piSessionCodec, sessionManagement: getAdapterSessionManagement("pi_local") ?? undefined, models: [], modelProfiles: piModelProfiles, listModels: listPiModels, supportsLocalAgentJwt: true, supportsInstructionsBundle: true, instructionsPathKey: "instructionsFilePath", requiresMaterializedRuntimeSkills: true, getRuntimeCommandSpec: (config) => buildNpmRuntimeCommandSpec(config, "pi", "@mariozechner/pi-coding-agent"), agentConfigurationDoc: piAgentConfigurationDoc, }; const adaptersByType = new Map(); // For builtin types that are overridden by an external adapter, we keep the // original builtin so it can be restored when the override is deactivated. const builtinFallbacks = new Map(); // Tracks which override types are currently deactivated (paused). When // paused, `getServerAdapter()` returns the builtin fallback instead of the // external. Persisted across reloads via the same disabled-adapters store. const pausedOverrides = new Set(); function registerBuiltInAdapters() { for (const adapter of [ acpxLocalAdapter, claudeLocalAdapter, codexLocalAdapter, paperclipRunnerAdapter, openCodeLocalAdapter, piLocalAdapter, cursorCloudAdapter, cursorLocalAdapter, geminiLocalAdapter, grokLocalAdapter, kimiLocalAdapter, hermesGatewayAdapter, hermesLocalAdapter, openclawGatewayAdapter, processAdapter, httpAdapter, ]) { adaptersByType.set(adapter.type, adapter); } } registerBuiltInAdapters(); // --------------------------------------------------------------------------- // Load external adapter plugins (e.g. droid_local) // // External adapter packages export createServerAdapter() which returns a // ServerAdapterModule. When the module provides its own sessionManagement // it is preserved; otherwise the host falls back to the built-in registry // lookup (so externals that override a built-in type inherit the builtin's // policy). This brings init-time registration to at-least-as-good behavior // as the hot-install path (routes/adapters.ts:179 -> registerServerAdapter): // both preserve module-provided sessionManagement, and init-time additionally // applies the registry fallback for externals overriding a built-in type. // --------------------------------------------------------------------------- /** Cached sync wrapper — the store is a simple JSON file read, safe to call frequently. */ function getDisabledAdapterTypesFromStore(): string[] { return getDisabledAdapterTypes(); } /** * Merge an external adapter module with host-provided session management. * * Module-provided `sessionManagement` takes precedence. When absent, fall * back to the hardcoded registry keyed by adapter type (so externals that * override a built-in — same `type` — inherit the builtin's policy). If * neither is available, `sessionManagement` remains `undefined`. * * Used by both the init-time IIFE below (external-adapter load pass on * server start) and the hot-install path in `routes/adapters.ts` * (`registerWithSessionManagement`), so the two load paths resolve * `sessionManagement` identically. */ export function resolveExternalAdapterRegistration( externalAdapter: ServerAdapterModule, ): ServerAdapterModule { return { ...externalAdapter, sessionManagement: externalAdapter.sessionManagement ?? getAdapterSessionManagement(externalAdapter.type) ?? undefined, }; } /** * Load external adapters from the plugin store and hardcoded sources. * Called once at module initialization. The promise is exported so that * callers (e.g. assertKnownAdapterType, app startup) can await completion * and avoid racing against the loading window. */ const externalAdaptersReady: Promise = (async () => { try { const externalAdapters = await buildExternalAdapters(); for (const externalAdapter of externalAdapters) { const overriding = BUILTIN_ADAPTER_TYPES.has(externalAdapter.type); if (overriding) { console.log( `[paperclip] External adapter "${externalAdapter.type}" overrides built-in adapter`, ); // Save the original builtin for later restoration. const existing = adaptersByType.get(externalAdapter.type); if (existing && !builtinFallbacks.has(externalAdapter.type)) { builtinFallbacks.set(externalAdapter.type, existing); } } adaptersByType.set( externalAdapter.type, resolveExternalAdapterRegistration(externalAdapter), ); } } catch (err) { console.error("[paperclip] Failed to load external adapters:", err); } })(); /** * Await this before validating adapter types to avoid race conditions * during server startup. External adapters are loaded asynchronously; * calling assertKnownAdapterType before this resolves will reject * valid external adapter types. */ export function waitForExternalAdapters(): Promise { return externalAdaptersReady; } export function registerServerAdapter(adapter: ServerAdapterModule): void { if (BUILTIN_ADAPTER_TYPES.has(adapter.type) && !builtinFallbacks.has(adapter.type)) { const existing = adaptersByType.get(adapter.type); if (existing) { builtinFallbacks.set(adapter.type, existing); } } adaptersByType.set(adapter.type, adapter); } export function unregisterServerAdapter(type: string): void { if (type === processAdapter.type || type === httpAdapter.type) return; if (builtinFallbacks.has(type)) { pausedOverrides.delete(type); const fallback = builtinFallbacks.get(type); if (fallback) { adaptersByType.set(type, fallback); } return; } if (BUILTIN_ADAPTER_TYPES.has(type)) { return; } adaptersByType.delete(type); } export function requireServerAdapter(type: string): ServerAdapterModule { const adapter = findActiveServerAdapter(type); if (!adapter) { throw new Error(`Unknown adapter type: ${type}`); } return adapter; } export function getServerAdapter(type: string): ServerAdapterModule { return findActiveServerAdapter(type) ?? processAdapter; } /** * Memoized view of PAPERCLIP_ADAPTER_MODELS, keyed by the raw env string so * tests (and live env mutation) that change the variable are still observed. * Parsing happens at most once per distinct raw value instead of per * `listAdapterModels` request, and malformed values fail SOFT here: we log the * parse error once (per distinct raw value) and fall back to adapter-discovered * models rather than throwing at request time. */ let adapterModelsEnvCache: { raw: string | undefined; value: ReturnType; } | null = null; function getDeclaredAdapterModels(): ReturnType { const raw = process.env.PAPERCLIP_ADAPTER_MODELS; if (adapterModelsEnvCache && adapterModelsEnvCache.raw === raw) { return adapterModelsEnvCache.value; } let value: ReturnType = null; try { value = parseAdapterModelsEnv(process.env); } catch (err) { console.error( "[paperclip] Invalid PAPERCLIP_ADAPTER_MODELS; ignoring declared model lists:", err, ); } adapterModelsEnvCache = { raw, value }; return value; } export async function listAdapterModels(type: string): Promise<{ id: string; label: string }[]> { const declaredModels = getDeclaredAdapterModels(); if (declaredModels && declaredModels[type]?.length) { return declaredModels[type].map((m) => ({ id: m.id, label: m.label ?? m.id })); } const adapter = findActiveServerAdapter(type); if (!adapter) return []; if (adapter.listModels) { const discovered = await adapter.listModels(); if (discovered.length > 0) return discovered; } return adapter.models ?? []; } export async function refreshAdapterModels(type: string): Promise<{ id: string; label: string }[]> { const adapter = findActiveServerAdapter(type); if (!adapter) return []; if (adapter.refreshModels) { const refreshed = await adapter.refreshModels(); if (refreshed.length > 0) return refreshed; } if (adapter.listModels) { const discovered = await adapter.listModels(); if (discovered.length > 0) return discovered; } return adapter.models ?? []; } export async function listAdapterModelProfiles(type: string): Promise { const adapter = findActiveServerAdapter(type); if (!adapter) return []; if (adapter.listModelProfiles) { const discovered = await adapter.listModelProfiles(); if (discovered.length > 0) return discovered; } return adapter.modelProfiles ?? []; } export function listServerAdapters(): ServerAdapterModule[] { return Array.from(adaptersByType.values()); } /** * List adapters excluding those that are disabled in settings. * Used for menus and agent creation flows — disabled adapters remain * functional for existing agents but hidden from selection. */ export function listEnabledServerAdapters(): ServerAdapterModule[] { const disabled = getDisabledAdapterTypesFromStore(); const disabledSet = disabled.length > 0 ? new Set(disabled) : null; return disabledSet ? Array.from(adaptersByType.values()).filter((a) => !disabledSet.has(a.type)) : Array.from(adaptersByType.values()); } export async function detectAdapterModel( type: string, ): Promise<{ model: string; provider: string; source: string; candidates?: string[] } | null> { const adapter = findActiveServerAdapter(type); if (!adapter?.detectModel) return null; const detected = await adapter.detectModel(); if (!detected) return null; return { model: detected.model, provider: detected.provider, source: detected.source, ...(detected.candidates?.length ? { candidates: detected.candidates } : {}), }; } // --------------------------------------------------------------------------- // Override pause / resume // --------------------------------------------------------------------------- /** * Pause or resume an external override for a builtin adapter type. * * - `paused = true` → subsequent calls to `getServerAdapter(type)` return * the builtin fallback instead of the external adapter. Already-running * agent sessions are unaffected (they hold a reference to the module they * started with). * * - `paused = false` → the external adapter is active again. * * Returns `true` if the state actually changed, `false` if the type is not * an override or was already in the requested state. */ export function setOverridePaused(type: string, paused: boolean): boolean { if (!builtinFallbacks.has(type)) return false; const wasPaused = pausedOverrides.has(type); if (paused && !wasPaused) { pausedOverrides.add(type); console.log(`[paperclip] Override paused for "${type}" — builtin adapter restored`); return true; } if (!paused && wasPaused) { pausedOverrides.delete(type); console.log(`[paperclip] Override resumed for "${type}" — external adapter active`); return true; } return false; } /** Check whether the external override for a builtin type is currently paused. */ export function isOverridePaused(type: string): boolean { return pausedOverrides.has(type); } /** Get the set of types whose overrides are currently paused. */ export function getPausedOverrides(): Set { return pausedOverrides; } export function findServerAdapter(type: string): ServerAdapterModule | null { return adaptersByType.get(type) ?? null; } export function findActiveServerAdapter(type: string): ServerAdapterModule | null { if (pausedOverrides.has(type)) { const fallback = builtinFallbacks.get(type); if (fallback) return fallback; } return adaptersByType.get(type) ?? null; }