diff --git a/src/cli/bootstrap-external.bootstrap-command.test.ts b/src/cli/bootstrap-external.bootstrap-command.test.ts index 62ca1aaf65a73..641205e4591c0 100644 --- a/src/cli/bootstrap-external.bootstrap-command.test.ts +++ b/src/cli/bootstrap-external.bootstrap-command.test.ts @@ -73,10 +73,7 @@ function createWebProfilesResponse(params?: { } as unknown as Response; } -function createJsonResponse(params?: { - status?: number; - payload?: unknown; -}): Response { +function createJsonResponse(params?: { status?: number; payload?: unknown }): Response { const status = params?.status ?? 200; return { status, @@ -137,9 +134,7 @@ function parseConfigSetValue(raw: string): unknown { function applyConfigSet(stateDir: string, keyPath: string, rawValue: string): void { const configPath = path.join(stateDir, "openclaw.json"); - const current = existsSync(configPath) - ? JSON.parse(readFileSync(configPath, "utf-8")) - : {}; + const current = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : {}; const segments = keyPath.split("."); let cursor: Record = current; for (const segment of segments.slice(0, -1)) { @@ -284,7 +279,7 @@ describe("bootstrapCommand always-onboard behavior", () => { promptMocks.confirm.mockImplementation(async () => promptMocks.confirmDecisions.length > 0 ? promptMocks.confirmDecisions.shift()! - : promptMocks.confirmDecision + : promptMocks.confirmDecision, ); promptMocks.select.mockReset(); promptMocks.select.mockImplementation(async () => promptMocks.selectValue); @@ -362,7 +357,11 @@ describe("bootstrapCommand always-onboard behavior", () => { stdout: `${JSON.stringify({ pending: pendingDeviceRequests, paired: pairedDevices })}\n`, }) as never; } - if (commandString === "openclaw" && argList.includes("devices") && argList.includes("approve")) { + if ( + commandString === "openclaw" && + argList.includes("devices") && + argList.includes("approve") + ) { const requestId = argList.at(-1) ?? ""; const match = pendingDeviceRequests.find((entry) => entry.requestId === requestId); if (!match) { @@ -371,7 +370,9 @@ describe("bootstrapCommand always-onboard behavior", () => { stderr: `request not found: ${requestId}\n`, }) as never; } - pendingDeviceRequests = pendingDeviceRequests.filter((entry) => entry.requestId !== requestId); + pendingDeviceRequests = pendingDeviceRequests.filter( + (entry) => entry.requestId !== requestId, + ); pairedDevices = [ ...pairedDevices, { @@ -385,11 +386,7 @@ describe("bootstrapCommand always-onboard behavior", () => { stdout: `Approved ${String(match.deviceId ?? "device")} (${requestId})\n`, }) as never; } - if ( - commandString === "openclaw" && - argList.includes("config") && - argList.includes("set") - ) { + if (commandString === "openclaw" && argList.includes("config") && argList.includes("set")) { const setIndex = argList.lastIndexOf("set"); const keyPath = argList[setIndex + 1]; const rawValue = argList[setIndex + 2]; @@ -791,9 +788,9 @@ describe("bootstrapCommand always-onboard behavior", () => { expect(updatedConfig.agents.defaults.model.primary).toBe( "dench-cloud/anthropic.claude-opus-4-6-v1", ); - expect(updatedConfig.agents.defaults.models["dench-cloud/anthropic.claude-opus-4-6-v1"]).toEqual( - expect.objectContaining({ alias: "Claude Opus 4.6 (Dench Cloud)" }), - ); + expect( + updatedConfig.agents.defaults.models["dench-cloud/anthropic.claude-opus-4-6-v1"], + ).toEqual(expect.objectContaining({ alias: "Claude Opus 4.6 (Dench Cloud)" })); expect(updatedConfig.plugins.allow).toContain("posthog-analytics"); expect(updatedConfig.plugins.allow).toContain("dench-ai-gateway"); expect(updatedConfig.plugins.allow).not.toContain("dench-cloud-provider"); @@ -981,13 +978,13 @@ describe("bootstrapCommand always-onboard behavior", () => { ); }); - expect(runtime.log).toHaveBeenCalledWith( - expect.stringContaining("D E N C H C L O U D"), - ); + expect(runtime.log).toHaveBeenCalledWith(expect.stringContaining("D E N C H C L O U D")); expect(runtime.log).toHaveBeenCalledWith(expect.stringContaining("App Integrations")); expect(promptMocks.confirm).toHaveBeenCalledWith( expect.objectContaining({ - message: expect.stringContaining("Continue with Dench Cloud? Recommended. API key: dench.com/api"), + message: expect.stringContaining( + "Continue with Dench Cloud? Recommended. API key: dench.com/api", + ), }), ); @@ -1903,7 +1900,9 @@ describe("bootstrapCommand always-onboard behavior", () => { ); const gatewayRestartCalledInAutofix = spawnCalls.some( (call) => - call.command === "openclaw" && call.args.includes("gateway") && call.args.includes("restart"), + call.command === "openclaw" && + call.args.includes("gateway") && + call.args.includes("restart"), ); const toolsProfileSetCall = spawnCalls.find( (call) => @@ -2031,7 +2030,7 @@ describe("bootstrapCommand always-onboard behavior", () => { expect(logMessages).toContain("gateway.err.log"); }); - it("stages exec and elevated commands config in raw JSON before onboard (webchat gets host exec from first boot)", async () => { + it("stages exec, elevated, and host approval config before onboard (webchat gets host exec from first boot)", async () => { const runtime: RuntimeEnv = { log: vi.fn(), error: vi.fn(), @@ -2049,6 +2048,8 @@ describe("bootstrapCommand always-onboard behavior", () => { const configPath = path.join(stateDir, "openclaw.json"); const config = JSON.parse(readFileSync(configPath, "utf-8")); + const execApprovalsPath = path.join(stateDir, "exec-approvals.json"); + const execApprovals = JSON.parse(readFileSync(execApprovalsPath, "utf-8")); expect(config.tools?.exec?.security).toBe("full"); expect(config.tools?.exec?.ask).toBe("off"); expect(config.tools?.elevated?.enabled).toBe(true); @@ -2056,6 +2057,9 @@ describe("bootstrapCommand always-onboard behavior", () => { expect(config.commands?.bash).toBe(true); expect(config.commands?.config).toBe(true); expect(config.agents?.defaults?.elevatedDefault).toBe("on"); + expect(execApprovals.version).toBe(1); + expect(execApprovals.defaults?.security).toBe("full"); + expect(execApprovals.defaults?.ask).toBe("off"); const onboardIndex = spawnCalls.findIndex( (c) => c.command === "openclaw" && c.args.includes("onboard"), @@ -2124,7 +2128,10 @@ describe("bootstrapCommand always-onboard behavior", () => { call.args.includes(key) && call.args.includes(value), ); - expect(postOnboardSetCall, `expected post-onboard config set for ${key}=${value}`).toBeDefined(); + expect( + postOnboardSetCall, + `expected post-onboard config set for ${key}=${value}`, + ).toBeDefined(); expect(postOnboardSetCall?.args).toEqual( expect.arrayContaining(["--profile", "dench", "config", "set", key, value]), ); @@ -2166,12 +2173,19 @@ describe("bootstrapCommand always-onboard behavior", () => { expect(elevatedEnabledCalls).toHaveLength(2); for (const call of elevatedEnabledCalls) { expect(call.args).toEqual( - expect.arrayContaining(["--profile", "dench", "config", "set", "tools.elevated.enabled", "true"]), + expect.arrayContaining([ + "--profile", + "dench", + "config", + "set", + "tools.elevated.enabled", + "true", + ]), ); } }); - it("preserves exec and elevated config in final openclaw.json after full bootstrap cycle", async () => { + it("preserves exec, elevated, and host approval config after full bootstrap cycle", async () => { const runtime: RuntimeEnv = { log: vi.fn(), error: vi.fn(), @@ -2189,6 +2203,8 @@ describe("bootstrapCommand always-onboard behavior", () => { const configPath = path.join(stateDir, "openclaw.json"); const finalConfig = JSON.parse(readFileSync(configPath, "utf-8")); + const execApprovalsPath = path.join(stateDir, "exec-approvals.json"); + const execApprovals = JSON.parse(readFileSync(execApprovalsPath, "utf-8")); expect(finalConfig.tools?.exec?.security).toBe("full"); expect(finalConfig.tools?.exec?.ask).toBe("off"); @@ -2199,6 +2215,56 @@ describe("bootstrapCommand always-onboard behavior", () => { expect(finalConfig.commands?.config).toBe(true); expect(finalConfig.agents?.defaults?.timeoutSeconds).toBe(86400); expect(finalConfig.tools?.profile).toBe("full"); + expect(execApprovals.version).toBe(1); + expect(execApprovals.defaults?.security).toBe("full"); + expect(execApprovals.defaults?.ask).toBe("off"); + }); + + it("preserves existing host exec approval rules while forcing permissive defaults", async () => { + writeFileSync( + path.join(stateDir, "exec-approvals.json"), + JSON.stringify({ + version: 7, + defaults: { + security: "deny", + ask: "on-miss", + }, + agents: { + "assistant:main": { + security: "deny", + ask: "on-request", + }, + }, + }), + ); + + const runtime: RuntimeEnv = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }; + + await bootstrapCommand( + { + nonInteractive: true, + noOpen: true, + skipUpdate: true, + }, + runtime, + ); + + const execApprovals = JSON.parse( + readFileSync(path.join(stateDir, "exec-approvals.json"), "utf-8"), + ); + expect(execApprovals.version).toBe(7); + expect(execApprovals.defaults?.security).toBe("full"); + expect(execApprovals.defaults?.ask).toBe("off"); + expect(execApprovals.agents).toEqual({ + "assistant:main": { + security: "deny", + ask: "on-request", + }, + }); }); it("strips npm_config_* env vars from npm global commands (prevents npx prefix hijack)", async () => { @@ -2224,8 +2290,7 @@ describe("bootstrapCommand always-onboard behavior", () => { const npmGlobalCalls = spawnCalls.filter( (call) => - call.command === "npm" && - (call.args.includes("-g") || call.args.includes("--global")), + call.command === "npm" && (call.args.includes("-g") || call.args.includes("--global")), ); expect(npmGlobalCalls.length).toBeGreaterThan(0); diff --git a/src/cli/bootstrap-external.ts b/src/cli/bootstrap-external.ts index 2b58f6a7c0be2..e925fc6484584 100644 --- a/src/cli/bootstrap-external.ts +++ b/src/cli/bootstrap-external.ts @@ -12,8 +12,8 @@ import { import path from "node:path"; import process from "node:process"; import { confirm, isCancel, select, spinner, text } from "@clack/prompts"; -import json5 from "json5"; import gradient from "gradient-string"; +import json5 from "json5"; import { isDaemonlessMode } from "../config/paths.js"; import { isTruthyEnvValue } from "../infra/env.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; @@ -52,6 +52,9 @@ const MAX_PORT_SCAN_ATTEMPTS = 100; const DEFAULT_BOOTSTRAP_ROLLOUT_STAGE = "default"; const DEFAULT_GATEWAY_LAUNCH_AGENT_LABEL = "ai.openclaw.gateway"; const REQUIRED_TOOLS_PROFILE = "full"; +const DEFAULT_EXEC_SECURITY = "full"; +const DEFAULT_EXEC_ASK = "off"; +const HOST_EXEC_APPROVALS_FILENAME = "exec-approvals.json"; const OPENCLAW_CLI_CHECK_CACHE_TTL_MS = 5 * 60_000; const OPENCLAW_UPDATE_PROMPT_SUPPRESS_AFTER_INSTALL_MS = 5 * 60_000; const OPENCLAW_CLI_CHECK_CACHE_FILE = "openclaw-cli-check.json"; @@ -445,7 +448,9 @@ function normalizeVersionOutput(raw: string | undefined): string | undefined { return first && first.length > 0 ? first : undefined; } -function parseOpenClawCalendarVersion(raw: string | undefined): [number, number, number] | undefined { +function parseOpenClawCalendarVersion( + raw: string | undefined, +): [number, number, number] | undefined { const match = raw?.match(/\b(\d{4})\.(\d+)\.(\d+)\b/u); if (!match) { return undefined; @@ -554,24 +559,28 @@ function readBundledPluginVersion(pluginDir: string): string | undefined { } function readConfiguredPluginAllowlist(stateDir: string): string[] { - const raw = readBootstrapConfig(stateDir) as { - plugins?: { - allow?: unknown; - }; - } | undefined; + const raw = readBootstrapConfig(stateDir) as + | { + plugins?: { + allow?: unknown; + }; + } + | undefined; return Array.isArray(raw?.plugins?.allow) ? raw.plugins.allow.filter((value): value is string => typeof value === "string") : []; } function readConfiguredPluginLoadPaths(stateDir: string): string[] { - const raw = readBootstrapConfig(stateDir) as { - plugins?: { - load?: { - paths?: unknown; - }; - }; - } | undefined; + const raw = readBootstrapConfig(stateDir) as + | { + plugins?: { + load?: { + paths?: unknown; + }; + }; + } + | undefined; return Array.isArray(raw?.plugins?.load?.paths) ? raw.plugins.load.paths.filter((value): value is string => typeof value === "string") : []; @@ -590,14 +599,7 @@ async function setOpenClawConfigJson(params: { }): Promise { await runOpenClawOrThrow({ openclawCommand: params.openclawCommand, - args: [ - "--profile", - params.profile, - "config", - "set", - params.key, - JSON.stringify(params.value), - ], + args: ["--profile", params.profile, "config", "set", params.key, JSON.stringify(params.value)], timeoutMs: 30_000, errorMessage: params.errorMessage, }); @@ -618,7 +620,9 @@ function readDenchIntegrationsMetadata(stateDir: string): Record): Record | undefined { +function readTtsElevenLabsConfig( + tts: Record, +): Record | undefined { return asRecord(tts.elevenlabs) ?? asRecord(asRecord(tts.providers)?.elevenlabs); } @@ -693,10 +697,7 @@ function disableDenchElevenLabsOverride( ) { delete elevenlabs.baseUrl; } - if ( - typeof elevenlabs.apiKey === "string" && - (!apiKey || elevenlabs.apiKey === apiKey) - ) { + if (typeof elevenlabs.apiKey === "string" && (!apiKey || elevenlabs.apiKey === apiKey)) { delete elevenlabs.apiKey; } if (Object.keys(elevenlabs).length === 0) { @@ -733,7 +734,7 @@ function applyDenchManagedIntegrationDefaults(params: { const tools = { ...(asRecord(nextConfig.tools) ?? {}) }; const deny = Array.isArray(tools.deny) - ? (tools.deny.filter((value): value is string => typeof value === "string")) + ? tools.deny.filter((value): value is string => typeof value === "string") : []; const web = { ...(asRecord(tools.web) ?? {}) }; const search = { ...(asRecord(web.search) ?? {}) }; @@ -809,12 +810,8 @@ async function syncBundledPlugins(params: { }; const currentAllow = readConfiguredPluginAllowlist(params.stateDir); const currentLoadPaths = readConfiguredPluginLoadPaths(params.stateDir); - const nextAllow = currentAllow.filter( - (value) => value !== "dench-cloud-provider", - ); - const nextLoadPaths = currentLoadPaths.filter( - (value) => !isLegacyDenchCloudPluginPath(value), - ); + const nextAllow = currentAllow.filter((value) => value !== "dench-cloud-provider"); + const nextLoadPaths = currentLoadPaths.filter((value) => !isLegacyDenchCloudPluginPath(value)); const legacyPluginDir = path.join(params.stateDir, "extensions", "dench-cloud-provider"); const hadLegacyEntry = entries["dench-cloud-provider"] !== undefined; const hadLegacyInstall = installs["dench-cloud-provider"] !== undefined; @@ -957,12 +954,12 @@ async function ensureDefaultWorkspacePath( /** * Stage all required pre-onboard config directly into `stateDir/openclaw.json` - * without going through the OpenClaw CLI. On a fresh install the "dench" - * profile doesn't exist yet (it's created by `openclaw onboard`), so any - * `openclaw config set` call fails. Writing the file directly sidesteps - * this while still ensuring the config is in place before onboard starts - * the daemon. The CLI-based re-application happens post-onboard once the - * profile is live. + * and `stateDir/exec-approvals.json` without going through the OpenClaw CLI. + * On a fresh install the "dench" profile doesn't exist yet (it's created by + * `openclaw onboard`), so any `openclaw config set` call fails. Writing the + * files directly sidesteps this while still ensuring the config is in place + * before onboard starts the daemon. The CLI-based re-application happens + * post-onboard once the profile is live. */ function stagePreOnboardConfig( stateDir: string, @@ -987,8 +984,8 @@ function stagePreOnboardConfig( const tools = { ...(asRecord(raw.tools) ?? {}) }; const exec = { ...(asRecord(tools.exec) ?? {}) }; - exec.security = "full"; - exec.ask = "off"; + exec.security = DEFAULT_EXEC_SECURITY; + exec.ask = DEFAULT_EXEC_ASK; tools.exec = exec; const elevated = { ...(asRecord(tools.elevated) ?? {}) }; elevated.enabled = true; @@ -1005,10 +1002,31 @@ function stagePreOnboardConfig( defaults.elevatedDefault = "on"; + mkdirSync(stateDir, { recursive: true }); + writeFileSync(path.join(stateDir, "openclaw.json"), `${JSON.stringify(raw, null, 2)}\n`); + ensureHostExecApprovalDefaults(stateDir); +} + +/** + * Recent OpenClaw builds enforce exec approvals from a host-side policy file + * in addition to `tools.exec` inside openclaw.json. Keep both layers aligned + * so bootstrap never leaves web chat waiting on a host approval prompt. + */ +function ensureHostExecApprovalDefaults(stateDir: string): void { + const raw = readHostExecApprovalsConfig(stateDir) ?? {}; + const defaults = { ...(asRecord(raw.defaults) ?? {}) }; + defaults.security = DEFAULT_EXEC_SECURITY; + defaults.ask = DEFAULT_EXEC_ASK; + const next = { + ...raw, + version: toFiniteNumber(raw.version) ?? 1, + defaults, + }; + mkdirSync(stateDir, { recursive: true }); writeFileSync( - path.join(stateDir, "openclaw.json"), - `${JSON.stringify(raw, null, 2)}\n`, + path.join(stateDir, HOST_EXEC_APPROVALS_FILENAME), + `${JSON.stringify(next, null, 2)}\n`, ); } @@ -1027,8 +1045,8 @@ async function ensureAgentDefaults(openclawCommand: string, profile: string): Pr ["agents.defaults.subagents.archiveAfterMinutes", "180"], ["agents.defaults.subagents.runTimeoutSeconds", "0"], ["tools.subagents.tools.deny", "[]"], - ["tools.exec.security", "full"], - ["tools.exec.ask", "off"], + ["tools.exec.security", DEFAULT_EXEC_SECURITY], + ["tools.exec.ask", DEFAULT_EXEC_ASK], ["tools.elevated.enabled", "true"], ["tools.elevated.allowFrom.webchat", '["*"]'], ["agents.defaults.elevatedDefault", "on"], @@ -1301,12 +1319,12 @@ function selectBootstrapDevicePairingRequest(pending: DeviceListEntry[]): { .filter((entry) => { const roles = resolveDeviceListEntryRoles(entry); const platformMatches = !entry.platform || entry.platform === process.platform; - return platformMatches && roles.includes("operator") && hasBootstrapDevicePairingScopes(entry); + return ( + platformMatches && roles.includes("operator") && hasBootstrapDevicePairingScopes(entry) + ); }) .map((entry) => ({ entry, score: scoreBootstrapDevicePairingRequest(entry) })) - .sort( - (a, b) => b.score - a.score || (b.entry.createdAtMs ?? 0) - (a.entry.createdAtMs ?? 0), - ); + .sort((a, b) => b.score - a.score || (b.entry.createdAtMs ?? 0) - (a.entry.createdAtMs ?? 0)); if (candidates.length === 0) { return { status: "none", detail: "no pending local operator pairing request found" }; } @@ -1904,13 +1922,7 @@ async function attemptGatewayAutoFix(params: { }, { name: "openclaw gateway install --force", - args: [ - "--profile", - params.profile, - "gateway", - "install", - "--force", - ], + args: ["--profile", params.profile, "gateway", "install", "--force"], timeoutMs: 2 * 60_000, }, { @@ -2062,6 +2074,18 @@ function readBootstrapConfig(stateDir: string): Record | undefi return undefined; } +function readHostExecApprovalsConfig(stateDir: string): Record | undefined { + const approvalsPath = path.join(stateDir, HOST_EXEC_APPROVALS_FILENAME); + if (!existsSync(approvalsPath)) { + return undefined; + } + try { + return asRecord(json5.parse(readFileSync(approvalsPath, "utf-8"))); + } catch { + return undefined; + } +} + function hasConfiguredComposioServer(_stateDir: string): boolean { return true; } @@ -2108,9 +2132,11 @@ function writeAuthProfileKey(stateDir: string, apiKey: string): void { if (existsSync(authPath)) { raw = json5.parse(readFileSync(authPath, "utf-8")); } - } catch { /* fresh file */ } + } catch { + /* fresh file */ + } - const profiles = ((raw.profiles ?? {}) as Record); + const profiles = (raw.profiles ?? {}) as Record; profiles["dench-cloud:default"] = { type: "api_key", provider: "dench-cloud", @@ -2132,11 +2158,13 @@ export function checkAgentAuth( if (!provider) { return { ok: false, detail: "No model provider configured." }; } - const rawConfig = readBootstrapConfig(stateDir) as { - models?: { - providers?: Record; - }; - } | undefined; + const rawConfig = readBootstrapConfig(stateDir) as + | { + models?: { + providers?: Record; + }; + } + | undefined; const customProvider = rawConfig?.models?.providers?.[provider]; if (customProvider && typeof customProvider === "object") { const apiKey = (customProvider as Record).apiKey; @@ -2399,9 +2427,9 @@ function logBootstrapChecklist(diagnostics: BootstrapDiagnostics, runtime: Runti function isExplicitDenchCloudRequest(opts: BootstrapOptions): boolean { return Boolean( opts.denchCloud || - opts.denchCloudApiKey?.trim() || - opts.denchCloudModel?.trim() || - opts.denchGatewayUrl?.trim(), + opts.denchCloudApiKey?.trim() || + opts.denchCloudModel?.trim() || + opts.denchGatewayUrl?.trim(), ); } @@ -2513,9 +2541,7 @@ function renderDenchCloudRecommendationBanner(): string { ].join(dot); const check = rich ? theme.success("✓") : "✓"; - const cta = rich - ? theme.success("Recommended for most users") - : "Recommended for most users"; + const cta = rich ? theme.success("Recommended for most users") : "Recommended for most users"; return [ "", @@ -2561,14 +2587,20 @@ function preStageDenchCloudConfig(params: { nextConfig.models = models; const tools = { ...asRecord(nextConfig.tools) }; - tools.alsoAllow = mergeAllowedTools(nextConfig.tools, (configPatch as Record).tools); + tools.alsoAllow = mergeAllowedTools( + nextConfig.tools, + (configPatch as Record).tools, + ); delete tools.allow; nextConfig.tools = tools; if (params.selectedModel) { const agents = { ...asRecord(nextConfig.agents) }; const defaults = { ...asRecord(agents.defaults) }; - defaults.model = { ...asRecord(defaults.model), primary: `dench-cloud/${params.selectedModel}` }; + defaults.model = { + ...asRecord(defaults.model), + primary: `dench-cloud/${params.selectedModel}`, + }; agents.defaults = defaults; nextConfig.agents = agents; } @@ -2624,7 +2656,10 @@ function rewriteDenchCloudTtsConfigFile(params: { elevenlabs.apiKey = params.apiKey; messages.tts = tts; nextConfig.messages = messages; - writeFileSync(path.join(params.stateDir, "openclaw.json"), `${JSON.stringify(nextConfig, null, 2)}\n`); + writeFileSync( + path.join(params.stateDir, "openclaw.json"), + `${JSON.stringify(nextConfig, null, 2)}\n`, + ); } function isExpectedTtsShapeValidationError( @@ -2669,9 +2704,10 @@ async function applyDenchCloudTtsConfig(params: { openclawCommand: params.openclawCommand, profile: params.profile, key: shape === "providers" ? "messages.tts.providers.elevenlabs" : "messages.tts.elevenlabs", - value: shape === "providers" - ? asRecord(asRecord(ttsConfig.providers)?.elevenlabs) - : asRecord(ttsConfig.elevenlabs), + value: + shape === "providers" + ? asRecord(asRecord(ttsConfig.providers)?.elevenlabs) + : asRecord(ttsConfig.elevenlabs), errorMessage: "Failed to configure ElevenLabs TTS via Dench Cloud gateway.", }); }; @@ -2699,13 +2735,15 @@ async function applyDenchCloudBootstrapConfig(params: { selectedModel: string; openClawVersion?: string; }): Promise { - const raw = readBootstrapConfig(params.stateDir) as { - agents?: { - defaults?: { - models?: unknown; - }; - }; - } | undefined; + const raw = readBootstrapConfig(params.stateDir) as + | { + agents?: { + defaults?: { + models?: unknown; + }; + }; + } + | undefined; const existingAgentModels = raw?.agents?.defaults?.models && typeof raw.agents.defaults.models === "object" ? (raw.agents.defaults.models as Record) @@ -2846,9 +2884,11 @@ async function resolveDenchCloudBootstrapSelection(params: { const wantsDenchCloud = explicitRequest ? true : await confirm({ - message: stylePromptMessage("Continue with Dench Cloud? Recommended. API key: dench.com/api"), - initialValue: existingDenchConfigured || !currentProvider, - }); + message: stylePromptMessage( + "Continue with Dench Cloud? Recommended. API key: dench.com/api", + ), + initialValue: existingDenchConfigured || !currentProvider, + }); if (isCancel(wantsDenchCloud) || !wantsDenchCloud) { return { enabled: false }; } @@ -2899,10 +2939,16 @@ async function resolveDenchCloudBootstrapSelection(params: { catalogSpinner?.stop("Models loaded."); } - const explicitModel = params.opts.denchCloudModel?.trim() || process.env.DENCH_CLOUD_MODEL?.trim(); - const preselected = resolveDenchCloudModel(catalog.models, explicitModel || existing.selectedModel); + const explicitModel = + params.opts.denchCloudModel?.trim() || process.env.DENCH_CLOUD_MODEL?.trim(); + const preselected = resolveDenchCloudModel( + catalog.models, + explicitModel || existing.selectedModel, + ); if (!preselected && explicitModel) { - params.runtime.log(theme.warn(`Configured Dench Cloud model "${explicitModel}" is unavailable.`)); + params.runtime.log( + theme.warn(`Configured Dench Cloud model "${explicitModel}" is unavailable.`), + ); } const selection = await promptForDenchCloudModel({ models: catalog.models, @@ -2923,9 +2969,7 @@ async function resolveDenchCloudBootstrapSelection(params: { verifySpinner?.stop("Dench Cloud ready."); } catch (error) { verifySpinner?.stop("Verification failed."); - params.runtime.log( - theme.warn(error instanceof Error ? error.message : String(error)), - ); + params.runtime.log(theme.warn(error instanceof Error ? error.message : String(error))); const retry = await confirm({ message: stylePromptMessage("Re-enter your Dench Cloud API key?"), initialValue: true, @@ -3131,11 +3175,11 @@ export async function bootstrapCommand( sourceDirName: "posthog-analytics", ...(process.env.POSTHOG_KEY ? { - enabled: true, - config: { - apiKey: process.env.POSTHOG_KEY, - }, - } + enabled: true, + config: { + apiKey: process.env.POSTHOG_KEY, + }, + } : {}), }, { @@ -3159,17 +3203,13 @@ export async function bootstrapCommand( pluginId: "apollo-enrichment", sourceDirName: "apollo-enrichment", enabled: denchCloudSelection.enabled, - ...(denchCloudSelection.enabled - ? { config: { enabled: true } } - : {}), + ...(denchCloudSelection.enabled ? { config: { enabled: true } } : {}), }, { pluginId: "exa-search", sourceDirName: "exa-search", enabled: denchCloudSelection.enabled, - ...(denchCloudSelection.enabled - ? { config: { enabled: true } } - : {}), + ...(denchCloudSelection.enabled ? { config: { enabled: true } } : {}), }, ];