UNPKG

@gguf/claw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,184 lines (1,172 loc) 77.1 kB
import { At as getResolvedLoggerSettings, Pt as CONFIG_PATH, Ut as resolveGatewayPort, Wt as resolveIsNixMode, Yt as resolveStateDir, _ as defaultRuntime, lt as shortenHomePath, n as isTruthyEnvValue, ot as resolveUserPath, pn as resolveRequiredHomeDir, qt as resolveOAuthDir, ut as sleep } from "./entry.js"; import { Gt as CODEX_CLI_PROFILE_ID, Ht as DEFAULT_PROVIDER, N as getModelRefStatus, S as updateAuthProfileStoreWithLock, U as resolveHooksGmailModel, V as resolveConfiguredModelRef, Vt as DEFAULT_MODEL, Wt as CLAUDE_CLI_PROFILE_ID, b as ensureAuthProfileStore, bt as resolveApiKeyForProvider, c as resolveApiKeyForProfile, p as repairOAuthProfileIdMismatch, s as resolveProfileUnusableUntilForDisplay } from "./auth-profiles-DFa1zzNy.js"; import { t as formatCliCommand } from "./command-format-D3syQOZg.js"; import { t as resolveOpenClawPackageRoot } from "./openclaw-root-B5pKN_cp.js"; import { n as runExec, t as runCommandWithTimeout } from "./exec-CBKBIMpA.js"; import { c as resolveAgentWorkspaceDir, i as resolveAgentDir, l as resolveDefaultAgentId, p as DEFAULT_AGENTS_FILENAME } from "./agent-scope-RzK9Zcks.js"; import { t as buildWorkspaceSkillStatus } from "./skills-status-qrtSNHRV.js"; import { l as writeConfigFile, o as readConfigFileSnapshot } from "./config-B2kL1ciP.js"; import { n as callGateway, t as buildGatewayConnectionDetails } from "./call-DXhJGwEy.js"; import { l as resolveGatewayBindHost, n as isLoopbackHost } from "./net-DA6Ow9GQ.js"; import { dn as formatRemainingShort, g as loadOpenClawPlugins, ln as DEFAULT_OAUTH_WARN_MS, un as buildAuthHealthSummary } from "./subagent-registry-C7Edpn23.js"; import { P as resolveMainSessionKey, V as cleanStaleLockFiles, a as loadSessionStore } from "./sessions-BD5dyLxb.js"; import { n as listChannelPlugins } from "./plugins-RqhjLCb6.js"; import { D as DEFAULT_SANDBOX_IMAGE, E as DEFAULT_SANDBOX_COMMON_IMAGE, T as DEFAULT_SANDBOX_BROWSER_IMAGE, f as resolveSandboxScope } from "./sandbox-CO-R8v6J.js"; import { i as resolveGatewayAuth } from "./auth-Dq2pFnjj.js"; import { i as formatPortDiagnostics, n as inspectPortUsage } from "./ports-59j-bA53.js"; import { c as resolveStorePath, n as resolveSessionFilePath, s as resolveSessionTranscriptsDirForAgent } from "./paths-CXpciDEv.js"; import { i as resolveMemoryBackendConfig } from "./memory-cli-C3uas9sI.js"; import { n as resolveMemorySearchConfig } from "./manager-D3ZmOwqt.js"; import { n as loadModelCatalog } from "./model-catalog-DLoDxnxL.js"; import { c as resolveGatewayLaunchAgentLabel, d as resolveGatewaySystemdServiceName, f as resolveGatewayWindowsTaskName, m as resolveNodeLaunchAgentLabel } from "./constants-ClfJ_wF3.js"; import { h as randomToken, n as applyWizardMetadata, p as printWizardHeader, s as guardCancel } from "./onboard-helpers-CEx2tGVB.js"; import { n as isWSLEnv, t as isWSL } from "./wsl-jdLWdiKy.js"; import { n as stylePromptMessage, r as stylePromptTitle, t as stylePromptHint } from "./prompt-style-DwCXob2h.js"; import { t as note$1 } from "./note-D3Xn5qjj.js"; import { t as resolveChannelDefaultAccountId } from "./helpers-UPrYYVP7.js"; import { t as collectChannelStatusIssues } from "./channels-status-issues-DgVhydeW.js"; import { a as gatewayInstallErrorHint, f as renderSystemNodeWarning, i as buildGatewayInstallPlan, m as resolveSystemNodeInfo, n as GATEWAY_DAEMON_RUNTIME_OPTIONS, t as DEFAULT_GATEWAY_DAEMON_RUNTIME } from "./daemon-runtime-DL5x9CWn.js"; import { r as isSystemdUserServiceAvailable } from "./systemd-BeKZd5oD.js"; import { a as launchAgentPlistExists, i as isLaunchAgentLoaded, o as repairLaunchAgentBootstrap, r as isLaunchAgentListed, s as resolveGatewayLogPaths, t as resolveGatewayService } from "./service-CIkh5YiN.js"; import { i as auditGatewayServiceConfig, n as renderSystemdUnavailableHints, o as needsNodeRuntimeMigration, r as SERVICE_AUDIT_CODES, s as formatRuntimeStatus, t as isSystemdUnavailableDetail } from "./systemd-hints-DCQYuPQQ.js"; import { t as readLastGatewayErrorLine } from "./diagnostics-CqIGmTJP.js"; import { n as renderGatewayServiceCleanupHints, t as findExtraGatewayServices } from "./inspect-DfaANKnj.js"; import { r as healthCommand } from "./health-75545GOF.js"; import { n as resolveControlUiDistIndexHealth, r as resolveControlUiDistIndexPathForRoot } from "./control-ui-assets-CwSEyfND.js"; import { t as resolveDmAllowState } from "./dm-policy-shared-BaTHScul.js"; import { n as runGatewayUpdate, t as resolveAgentSessionDirs } from "./session-dirs-DzJ9UfQe.js"; import { n as logConfigUpdated } from "./logging-BGewRZy0.js"; import { n as detectLegacyStateMigrations, r as runLegacyStateMigrations, t as loadAndMaybeMigrateDoctorConfig } from "./doctor-config-flow-Cr5VX2mY.js"; import { t as ensureSystemdUserLingerInteractive } from "./systemd-linger-CgYzp_8H.js"; import { t as formatHealthCheckFailure } from "./health-format-DkjSgkDx.js"; import { n as doctorShellCompletion } from "./doctor-completion-CkzolxKG.js"; import { execFile } from "node:child_process"; import os from "node:os"; import path from "node:path"; import fs from "node:fs"; import { promisify } from "node:util"; import fs$1 from "node:fs/promises"; import { confirm, intro, outro, select } from "@clack/prompts"; //#region src/commands/doctor-auth.ts async function maybeRepairAnthropicOAuthProfileId(cfg, prompter) { const repair = repairOAuthProfileIdMismatch({ cfg, store: ensureAuthProfileStore(), provider: "anthropic", legacyProfileId: "anthropic:default" }); if (!repair.migrated || repair.changes.length === 0) return cfg; note$1(repair.changes.map((c) => `- ${c}`).join("\n"), "Auth profiles"); if (!await prompter.confirm({ message: "Update Anthropic OAuth profile id in config now?", initialValue: true })) return cfg; return repair.config; } function pruneAuthOrder(order, profileIds) { if (!order) return { next: order, changed: false }; let changed = false; const next = {}; for (const [provider, list] of Object.entries(order)) { const filtered = list.filter((id) => !profileIds.has(id)); if (filtered.length !== list.length) changed = true; if (filtered.length > 0) next[provider] = filtered; } return { next: Object.keys(next).length > 0 ? next : void 0, changed }; } function pruneAuthProfiles(cfg, profileIds) { const profiles = cfg.auth?.profiles; const order = cfg.auth?.order; const nextProfiles = profiles ? { ...profiles } : void 0; let changed = false; if (nextProfiles) { for (const id of profileIds) if (id in nextProfiles) { delete nextProfiles[id]; changed = true; } } const prunedOrder = pruneAuthOrder(order, profileIds); if (prunedOrder.changed) changed = true; if (!changed) return { next: cfg, changed: false }; const nextAuth = nextProfiles || prunedOrder.next ? { ...cfg.auth, profiles: nextProfiles && Object.keys(nextProfiles).length > 0 ? nextProfiles : void 0, order: prunedOrder.next } : void 0; return { next: { ...cfg, auth: nextAuth }, changed: true }; } async function maybeRemoveDeprecatedCliAuthProfiles(cfg, prompter) { const store = ensureAuthProfileStore(void 0, { allowKeychainPrompt: false }); const deprecated = /* @__PURE__ */ new Set(); if (store.profiles[CLAUDE_CLI_PROFILE_ID] || cfg.auth?.profiles?.[CLAUDE_CLI_PROFILE_ID]) deprecated.add(CLAUDE_CLI_PROFILE_ID); if (store.profiles[CODEX_CLI_PROFILE_ID] || cfg.auth?.profiles?.[CODEX_CLI_PROFILE_ID]) deprecated.add(CODEX_CLI_PROFILE_ID); if (deprecated.size === 0) return cfg; const lines = ["Deprecated external CLI auth profiles detected (no longer supported):"]; if (deprecated.has(CLAUDE_CLI_PROFILE_ID)) lines.push(`- ${CLAUDE_CLI_PROFILE_ID} (Anthropic): use setup-token → ${formatCliCommand("openclaw models auth setup-token")}`); if (deprecated.has(CODEX_CLI_PROFILE_ID)) lines.push(`- ${CODEX_CLI_PROFILE_ID} (OpenAI Codex): use OAuth → ${formatCliCommand("openclaw models auth login --provider openai-codex")}`); note$1(lines.join("\n"), "Auth profiles"); if (!await prompter.confirmRepair({ message: "Remove deprecated CLI auth profiles now?", initialValue: true })) return cfg; await updateAuthProfileStoreWithLock({ updater: (nextStore) => { let mutated = false; for (const id of deprecated) { if (nextStore.profiles[id]) { delete nextStore.profiles[id]; mutated = true; } if (nextStore.usageStats?.[id]) { delete nextStore.usageStats[id]; mutated = true; } } if (nextStore.order) for (const [provider, list] of Object.entries(nextStore.order)) { const filtered = list.filter((id) => !deprecated.has(id)); if (filtered.length !== list.length) { mutated = true; if (filtered.length > 0) nextStore.order[provider] = filtered; else delete nextStore.order[provider]; } } if (nextStore.lastGood) { for (const [provider, profileId] of Object.entries(nextStore.lastGood)) if (deprecated.has(profileId)) { delete nextStore.lastGood[provider]; mutated = true; } } return mutated; } }); const pruned = pruneAuthProfiles(cfg, deprecated); if (pruned.changed) note$1(Array.from(deprecated.values()).map((id) => `- removed ${id} from config`).join("\n"), "Doctor changes"); return pruned.next; } function formatAuthIssueHint(issue) { if (issue.provider === "anthropic" && issue.profileId === CLAUDE_CLI_PROFILE_ID) return `Deprecated profile. Use ${formatCliCommand("openclaw models auth setup-token")} or ${formatCliCommand("openclaw configure")}.`; if (issue.provider === "openai-codex" && issue.profileId === CODEX_CLI_PROFILE_ID) return `Deprecated profile. Use ${formatCliCommand("openclaw models auth login --provider openai-codex")} or ${formatCliCommand("openclaw configure")}.`; return `Re-auth via \`${formatCliCommand("openclaw configure")}\` or \`${formatCliCommand("openclaw onboard")}\`.`; } function formatAuthIssueLine(issue) { const remaining = issue.remainingMs !== void 0 ? ` (${formatRemainingShort(issue.remainingMs)})` : ""; const hint = formatAuthIssueHint(issue); return `- ${issue.profileId}: ${issue.status}${remaining}${hint ? ` — ${hint}` : ""}`; } async function noteAuthProfileHealth(params) { const store = ensureAuthProfileStore(void 0, { allowKeychainPrompt: params.allowKeychainPrompt }); const unusable = (() => { const now = Date.now(); const out = []; for (const profileId of Object.keys(store.usageStats ?? {})) { const until = resolveProfileUnusableUntilForDisplay(store, profileId); if (!until || now >= until) continue; const stats = store.usageStats?.[profileId]; const remaining = formatRemainingShort(until - now); const kind = typeof stats?.disabledUntil === "number" && now < stats.disabledUntil ? `disabled${stats.disabledReason ? `:${stats.disabledReason}` : ""}` : "cooldown"; const hint = kind.startsWith("disabled:billing") ? "Top up credits (provider billing) or switch provider." : "Wait for cooldown or switch provider."; out.push(`- ${profileId}: ${kind} (${remaining})${hint ? ` — ${hint}` : ""}`); } return out; })(); if (unusable.length > 0) note$1(unusable.join("\n"), "Auth profile cooldowns"); let summary = buildAuthHealthSummary({ store, cfg: params.cfg, warnAfterMs: DEFAULT_OAUTH_WARN_MS }); const findIssues = () => summary.profiles.filter((profile) => (profile.type === "oauth" || profile.type === "token") && (profile.status === "expired" || profile.status === "expiring" || profile.status === "missing")); let issues = findIssues(); if (issues.length === 0) return; if (await params.prompter.confirmRepair({ message: "Refresh expiring OAuth tokens now? (static tokens need re-auth)", initialValue: true })) { const refreshTargets = issues.filter((issue) => issue.type === "oauth" && [ "expired", "expiring", "missing" ].includes(issue.status)); const errors = []; for (const profile of refreshTargets) try { await resolveApiKeyForProfile({ cfg: params.cfg, store, profileId: profile.profileId }); } catch (err) { errors.push(`- ${profile.profileId}: ${err instanceof Error ? err.message : String(err)}`); } if (errors.length > 0) note$1(errors.join("\n"), "OAuth refresh errors"); summary = buildAuthHealthSummary({ store: ensureAuthProfileStore(void 0, { allowKeychainPrompt: false }), cfg: params.cfg, warnAfterMs: DEFAULT_OAUTH_WARN_MS }); issues = findIssues(); } if (issues.length > 0) note$1(issues.map((issue) => formatAuthIssueLine({ profileId: issue.profileId, provider: issue.provider, status: issue.status, remainingMs: issue.remainingMs })).join("\n"), "Model auth"); } //#endregion //#region src/commands/doctor-format.ts function formatGatewayRuntimeSummary(runtime) { return formatRuntimeStatus(runtime); } function buildGatewayRuntimeHints(runtime, options = {}) { const hints = []; if (!runtime) return hints; const platform = options.platform ?? process.platform; const env = options.env ?? process.env; const fileLog = (() => { try { return getResolvedLoggerSettings().file; } catch { return null; } })(); if (platform === "linux" && isSystemdUnavailableDetail(runtime.detail)) { hints.push(...renderSystemdUnavailableHints({ wsl: isWSLEnv() })); if (fileLog) hints.push(`File logs: ${fileLog}`); return hints; } if (runtime.cachedLabel && platform === "darwin") { const label = resolveGatewayLaunchAgentLabel(env.OPENCLAW_PROFILE); hints.push(`LaunchAgent label cached but plist missing. Clear with: launchctl bootout gui/$UID/${label}`); hints.push(`Then reinstall: ${formatCliCommand("openclaw gateway install", env)}`); } if (runtime.missingUnit) { hints.push(`Service not installed. Run: ${formatCliCommand("openclaw gateway install", env)}`); if (fileLog) hints.push(`File logs: ${fileLog}`); return hints; } if (runtime.status === "stopped") { hints.push("Service is loaded but not running (likely exited immediately)."); if (fileLog) hints.push(`File logs: ${fileLog}`); if (platform === "darwin") { const logs = resolveGatewayLogPaths(env); hints.push(`Launchd stdout (if installed): ${logs.stdoutPath}`); hints.push(`Launchd stderr (if installed): ${logs.stderrPath}`); } else if (platform === "linux") { const unit = resolveGatewaySystemdServiceName(env.OPENCLAW_PROFILE); hints.push(`Logs: journalctl --user -u ${unit}.service -n 200 --no-pager`); } else if (platform === "win32") { const task = resolveGatewayWindowsTaskName(env.OPENCLAW_PROFILE); hints.push(`Logs: schtasks /Query /TN "${task}" /V /FO LIST`); } } return hints; } //#endregion //#region src/commands/doctor-gateway-daemon-flow.ts async function maybeRepairLaunchAgentBootstrap(params) { if (process.platform !== "darwin") return false; if (!await isLaunchAgentListed({ env: params.env })) return false; if (await isLaunchAgentLoaded({ env: params.env })) return false; if (!await launchAgentPlistExists(params.env)) return false; note$1("LaunchAgent is listed but not loaded in launchd.", `${params.title} LaunchAgent`); if (!await params.prompter.confirmSkipInNonInteractive({ message: `Repair ${params.title} LaunchAgent bootstrap now?`, initialValue: true })) return false; params.runtime.log(`Bootstrapping ${params.title} LaunchAgent...`); const repair = await repairLaunchAgentBootstrap({ env: params.env }); if (!repair.ok) { params.runtime.error(`${params.title} LaunchAgent bootstrap failed: ${repair.detail ?? "unknown error"}`); return false; } if (!await isLaunchAgentLoaded({ env: params.env })) { params.runtime.error(`${params.title} LaunchAgent still not loaded after repair.`); return false; } note$1(`${params.title} LaunchAgent repaired.`, `${params.title} LaunchAgent`); return true; } async function maybeRepairGatewayDaemon(params) { if (params.healthOk) return; const service = resolveGatewayService(); let loaded = false; try { loaded = await service.isLoaded({ env: process.env }); } catch { loaded = false; } let serviceRuntime; if (loaded) serviceRuntime = await service.readRuntime(process.env).catch(() => void 0); if (process.platform === "darwin" && params.cfg.gateway?.mode !== "remote") { const gatewayRepaired = await maybeRepairLaunchAgentBootstrap({ env: process.env, title: "Gateway", runtime: params.runtime, prompter: params.prompter }); await maybeRepairLaunchAgentBootstrap({ env: { ...process.env, OPENCLAW_LAUNCHD_LABEL: resolveNodeLaunchAgentLabel() }, title: "Node", runtime: params.runtime, prompter: params.prompter }); if (gatewayRepaired) { loaded = await service.isLoaded({ env: process.env }); if (loaded) serviceRuntime = await service.readRuntime(process.env).catch(() => void 0); } } if (params.cfg.gateway?.mode !== "remote") { const diagnostics = await inspectPortUsage(resolveGatewayPort(params.cfg, process.env)); if (diagnostics.status === "busy") note$1(formatPortDiagnostics(diagnostics).join("\n"), "Gateway port"); else if (loaded && serviceRuntime?.status === "running") { const lastError = await readLastGatewayErrorLine(process.env); if (lastError) note$1(`Last gateway error: ${lastError}`, "Gateway"); } } if (!loaded) { if (process.platform === "linux") { if (!await isSystemdUserServiceAvailable().catch(() => false)) { note$1(renderSystemdUnavailableHints({ wsl: await isWSL() }).join("\n"), "Gateway"); return; } } note$1("Gateway service not installed.", "Gateway"); if (params.cfg.gateway?.mode !== "remote") { if (await params.prompter.confirmSkipInNonInteractive({ message: "Install gateway service now?", initialValue: true })) { const daemonRuntime = await params.prompter.select({ message: "Gateway service runtime", options: GATEWAY_DAEMON_RUNTIME_OPTIONS, initialValue: DEFAULT_GATEWAY_DAEMON_RUNTIME }, DEFAULT_GATEWAY_DAEMON_RUNTIME); const port = resolveGatewayPort(params.cfg, process.env); const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({ env: process.env, port, token: params.cfg.gateway?.auth?.token ?? process.env.OPENCLAW_GATEWAY_TOKEN, runtime: daemonRuntime, warn: (message, title) => note$1(message, title), config: params.cfg }); try { await service.install({ env: process.env, stdout: process.stdout, programArguments, workingDirectory, environment }); } catch (err) { note$1(`Gateway service install failed: ${String(err)}`, "Gateway"); note$1(gatewayInstallErrorHint(), "Gateway"); } } } return; } const summary = formatGatewayRuntimeSummary(serviceRuntime); const hints = buildGatewayRuntimeHints(serviceRuntime, { platform: process.platform, env: process.env }); if (summary || hints.length > 0) { const lines = []; if (summary) lines.push(`Runtime: ${summary}`); lines.push(...hints); note$1(lines.join("\n"), "Gateway"); } if (serviceRuntime?.status !== "running") { if (await params.prompter.confirmSkipInNonInteractive({ message: "Start gateway service now?", initialValue: true })) { await service.restart({ env: process.env, stdout: process.stdout }); await sleep(1500); } } if (process.platform === "darwin") { const label = resolveGatewayLaunchAgentLabel(process.env.OPENCLAW_PROFILE); note$1(`LaunchAgent loaded; stopping requires "${formatCliCommand("openclaw gateway stop")}" or launchctl bootout gui/$UID/${label}.`, "Gateway"); } if (serviceRuntime?.status === "running") { if (await params.prompter.confirmSkipInNonInteractive({ message: "Restart gateway service now?", initialValue: true })) { await service.restart({ env: process.env, stdout: process.stdout }); await sleep(1500); try { await healthCommand({ json: false, timeoutMs: 1e4 }, params.runtime); } catch (err) { if (String(err).includes("gateway closed")) { note$1("Gateway not running.", "Gateway"); note$1(params.gatewayDetailsMessage, "Gateway connection"); } else params.runtime.error(formatHealthCheckFailure(err)); } } } } //#endregion //#region src/commands/doctor-gateway-health.ts async function checkGatewayHealth(params) { const gatewayDetails = buildGatewayConnectionDetails({ config: params.cfg }); const timeoutMs = typeof params.timeoutMs === "number" && params.timeoutMs > 0 ? params.timeoutMs : 1e4; let healthOk = false; try { await healthCommand({ json: false, timeoutMs, config: params.cfg }, params.runtime); healthOk = true; } catch (err) { if (String(err).includes("gateway closed")) { note$1("Gateway not running.", "Gateway"); note$1(gatewayDetails.message, "Gateway connection"); } else params.runtime.error(formatHealthCheckFailure(err)); } if (healthOk) try { const issues = collectChannelStatusIssues(await callGateway({ method: "channels.status", params: { probe: true, timeoutMs: 5e3 }, timeoutMs: 6e3 })); if (issues.length > 0) note$1(issues.map((issue) => `- ${issue.channel} ${issue.accountId}: ${issue.message}${issue.fix ? ` (${issue.fix})` : ""}`).join("\n"), "Channel warnings"); } catch {} return { healthOk }; } //#endregion //#region src/commands/doctor-gateway-services.ts const execFileAsync$1 = promisify(execFile); function detectGatewayRuntime(programArguments) { const first = programArguments?.[0]; if (first) { const base = path.basename(first).toLowerCase(); if (base === "bun" || base === "bun.exe") return "bun"; if (base === "node" || base === "node.exe") return "node"; } return DEFAULT_GATEWAY_DAEMON_RUNTIME; } function findGatewayEntrypoint(programArguments) { if (!programArguments || programArguments.length === 0) return null; const gatewayIndex = programArguments.indexOf("gateway"); if (gatewayIndex <= 0) return null; return programArguments[gatewayIndex - 1] ?? null; } function normalizeExecutablePath(value) { return path.resolve(value); } function resolveGatewayAuthToken(cfg, env) { const configToken = cfg.gateway?.auth?.token?.trim(); if (configToken) return configToken; return (env.OPENCLAW_GATEWAY_TOKEN ?? env.CLAWDBOT_GATEWAY_TOKEN)?.trim() || void 0; } function extractDetailPath(detail, prefix) { if (!detail.startsWith(prefix)) return null; const value = detail.slice(prefix.length).trim(); return value.length > 0 ? value : null; } async function cleanupLegacyLaunchdService(params) { await execFileAsync$1("launchctl", [ "bootout", typeof process.getuid === "function" ? `gui/${process.getuid()}` : "gui/501", params.plistPath ]).catch(() => void 0); await execFileAsync$1("launchctl", ["unload", params.plistPath]).catch(() => void 0); const trashDir = path.join(os.homedir(), ".Trash"); try { await fs$1.mkdir(trashDir, { recursive: true }); } catch {} try { await fs$1.access(params.plistPath); } catch { return null; } const dest = path.join(trashDir, `${params.label}-${Date.now()}.plist`); try { await fs$1.rename(params.plistPath, dest); return dest; } catch { return null; } } async function maybeRepairGatewayServiceConfig(cfg, mode, runtime, prompter) { if (resolveIsNixMode(process.env)) { note$1("Nix mode detected; skip service updates.", "Gateway"); return; } if (mode === "remote") { note$1("Gateway mode is remote; skipped local service audit.", "Gateway"); return; } const service = resolveGatewayService(); let command = null; try { command = await service.readCommand(process.env); } catch { command = null; } if (!command) return; const expectedGatewayToken = resolveGatewayAuthToken(cfg, process.env); const audit = await auditGatewayServiceConfig({ env: process.env, command, expectedGatewayToken }); const needsNodeRuntime = needsNodeRuntimeMigration(audit.issues); const systemNodeInfo = needsNodeRuntime ? await resolveSystemNodeInfo({ env: process.env }) : null; const systemNodePath = systemNodeInfo?.supported ? systemNodeInfo.path : null; if (needsNodeRuntime && !systemNodePath) { const warning = renderSystemNodeWarning(systemNodeInfo); if (warning) note$1(warning, "Gateway runtime"); note$1("System Node 22+ not found. Install via Homebrew/apt/choco and rerun doctor to migrate off Bun/version managers.", "Gateway runtime"); } const port = resolveGatewayPort(cfg, process.env); const runtimeChoice = detectGatewayRuntime(command.programArguments); const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({ env: process.env, port, token: expectedGatewayToken, runtime: needsNodeRuntime && systemNodePath ? "node" : runtimeChoice, nodePath: systemNodePath ?? void 0, warn: (message, title) => note$1(message, title), config: cfg }); const expectedEntrypoint = findGatewayEntrypoint(programArguments); const currentEntrypoint = findGatewayEntrypoint(command.programArguments); if (expectedEntrypoint && currentEntrypoint && normalizeExecutablePath(expectedEntrypoint) !== normalizeExecutablePath(currentEntrypoint)) audit.issues.push({ code: SERVICE_AUDIT_CODES.gatewayEntrypointMismatch, message: "Gateway service entrypoint does not match the current install.", detail: `${currentEntrypoint} -> ${expectedEntrypoint}`, level: "recommended" }); if (audit.issues.length === 0) return; note$1(audit.issues.map((issue) => issue.detail ? `- ${issue.message} (${issue.detail})` : `- ${issue.message}`).join("\n"), "Gateway service config"); const needsAggressive = audit.issues.filter((issue) => issue.level === "aggressive").length > 0; if (needsAggressive && !prompter.shouldForce) note$1("Custom or unexpected service edits detected. Rerun with --force to overwrite.", "Gateway service config"); if (!(needsAggressive ? await prompter.confirmAggressive({ message: "Overwrite gateway service config with current defaults now?", initialValue: Boolean(prompter.shouldForce) }) : await prompter.confirmRepair({ message: "Update gateway service config to the recommended defaults now?", initialValue: true }))) return; try { await service.install({ env: process.env, stdout: process.stdout, programArguments, workingDirectory, environment }); } catch (err) { runtime.error(`Gateway service update failed: ${String(err)}`); } } async function maybeScanExtraGatewayServices(options, runtime, prompter) { const extraServices = await findExtraGatewayServices(process.env, { deep: options.deep }); if (extraServices.length === 0) return; note$1(extraServices.map((svc) => `- ${svc.label} (${svc.scope}, ${svc.detail})`).join("\n"), "Other gateway-like services detected"); const legacyServices = extraServices.filter((svc) => svc.legacy === true); if (legacyServices.length > 0) { if (await prompter.confirmSkipInNonInteractive({ message: "Remove legacy gateway services (clawdbot/moltbot) now?", initialValue: true })) { const removed = []; const failed = []; for (const svc of legacyServices) { if (svc.platform !== "darwin") { failed.push(`${svc.label} (${svc.platform})`); continue; } if (svc.scope !== "user") { failed.push(`${svc.label} (${svc.scope})`); continue; } const plistPath = extractDetailPath(svc.detail, "plist:"); if (!plistPath) { failed.push(`${svc.label} (missing plist path)`); continue; } const dest = await cleanupLegacyLaunchdService({ label: svc.label, plistPath }); removed.push(dest ? `${svc.label} -> ${dest}` : svc.label); } if (removed.length > 0) note$1(removed.map((line) => `- ${line}`).join("\n"), "Legacy gateway removed"); if (failed.length > 0) note$1(failed.map((line) => `- ${line}`).join("\n"), "Legacy gateway cleanup skipped"); if (removed.length > 0) runtime.log("Legacy gateway services removed. Installing OpenClaw gateway next."); } } const cleanupHints = renderGatewayServiceCleanupHints(); if (cleanupHints.length > 0) note$1(cleanupHints.map((hint) => `- ${hint}`).join("\n"), "Cleanup hints"); note$1([ "Recommendation: run a single gateway per machine for most setups.", "One gateway supports multiple agents.", "If you need multiple gateways (e.g., a rescue bot on the same host), isolate ports + config/state (see docs: /gateway#multiple-gateways-same-host)." ].join("\n"), "Gateway recommendation"); } //#endregion //#region src/commands/doctor-install.ts function noteSourceInstallIssues(root) { if (!root) return; const workspaceMarker = path.join(root, "pnpm-workspace.yaml"); if (!fs.existsSync(workspaceMarker)) return; const warnings = []; const nodeModules = path.join(root, "node_modules"); const pnpmStore = path.join(nodeModules, ".pnpm"); const tsxBin = path.join(nodeModules, ".bin", "tsx"); const srcEntry = path.join(root, "src", "entry.ts"); if (fs.existsSync(nodeModules) && !fs.existsSync(pnpmStore)) warnings.push("- node_modules was not installed by pnpm (missing node_modules/.pnpm). Run: pnpm install"); if (fs.existsSync(path.join(root, "package-lock.json"))) warnings.push("- package-lock.json present in a pnpm workspace. If you ran npm install, remove it and reinstall with pnpm."); if (fs.existsSync(srcEntry) && !fs.existsSync(tsxBin)) warnings.push("- tsx binary is missing for source runs. Run: pnpm install"); if (warnings.length > 0) note$1(warnings.join("\n"), "Install"); } //#endregion //#region src/commands/doctor-memory-search.ts /** * Check whether memory search has a usable embedding provider. * Runs as part of `openclaw doctor` — config-only, no network calls. */ async function noteMemorySearchHealth(cfg) { const agentId = resolveDefaultAgentId(cfg); const agentDir = resolveAgentDir(cfg, agentId); const resolved = resolveMemorySearchConfig(cfg, agentId); const hasRemoteApiKey = Boolean(resolved?.remote?.apiKey?.trim()); if (!resolved) { note$1("Memory search is explicitly disabled (enabled: false).", "Memory search"); return; } if (resolveMemoryBackendConfig({ cfg, agentId }).backend === "qmd") return; if (resolved.provider !== "auto") { if (resolved.provider === "local") { if (hasLocalEmbeddings(resolved.local)) return; note$1([ "Memory search provider is set to \"local\" but no local model file was found.", "", "Fix (pick one):", `- Install node-llama-cpp and set a local model path in config`, `- Switch to a remote provider: ${formatCliCommand("openclaw config set agents.defaults.memorySearch.provider openai")}`, "", `Verify: ${formatCliCommand("openclaw memory status --deep")}` ].join("\n"), "Memory search"); return; } if (hasRemoteApiKey || await hasApiKeyForProvider(resolved.provider, cfg, agentDir)) return; const envVar = providerEnvVar(resolved.provider); note$1([ `Memory search provider is set to "${resolved.provider}" but no API key was found.`, `Semantic recall will not work without a valid API key.`, "", "Fix (pick one):", `- Set ${envVar} in your environment`, `- Add credentials: ${formatCliCommand(`openclaw auth add --provider ${resolved.provider}`)}`, `- To disable: ${formatCliCommand("openclaw config set agents.defaults.memorySearch.enabled false")}`, "", `Verify: ${formatCliCommand("openclaw memory status --deep")}` ].join("\n"), "Memory search"); return; } if (hasLocalEmbeddings(resolved.local)) return; for (const provider of [ "openai", "gemini", "voyage" ]) if (hasRemoteApiKey || await hasApiKeyForProvider(provider, cfg, agentDir)) return; note$1([ "Memory search is enabled but no embedding provider is configured.", "Semantic recall will not work without an embedding provider.", "", "Fix (pick one):", "- Set OPENAI_API_KEY or GEMINI_API_KEY in your environment", `- Add credentials: ${formatCliCommand("openclaw auth add --provider openai")}`, `- For local embeddings: configure agents.defaults.memorySearch.provider and local model path`, `- To disable: ${formatCliCommand("openclaw config set agents.defaults.memorySearch.enabled false")}`, "", `Verify: ${formatCliCommand("openclaw memory status --deep")}` ].join("\n"), "Memory search"); } function hasLocalEmbeddings(local) { const modelPath = local.modelPath?.trim(); if (!modelPath) return false; if (/^(hf:|https?:)/i.test(modelPath)) return true; const resolved = resolveUserPath(modelPath); try { return fs.statSync(resolved).isFile(); } catch { return false; } } async function hasApiKeyForProvider(provider, cfg, agentDir) { const authProvider = provider === "gemini" ? "google" : provider; try { await resolveApiKeyForProvider({ provider: authProvider, cfg, agentDir }); return true; } catch { return false; } } function providerEnvVar(provider) { switch (provider) { case "openai": return "OPENAI_API_KEY"; case "gemini": return "GEMINI_API_KEY"; case "voyage": return "VOYAGE_API_KEY"; default: return `${provider.toUpperCase()}_API_KEY`; } } //#endregion //#region src/commands/doctor-platform-notes.ts const execFileAsync = promisify(execFile); function resolveHomeDir() { return process.env.HOME ?? os.homedir(); } async function noteMacLaunchAgentOverrides() { if (process.platform !== "darwin") return; const home = resolveHomeDir(); const markerPath = [path.join(home, ".openclaw", "disable-launchagent")].find((candidate) => fs.existsSync(candidate)); if (!markerPath) return; const displayMarkerPath = shortenHomePath(markerPath); note$1([ `- LaunchAgent writes are disabled via ${displayMarkerPath}.`, "- To restore default behavior:", ` rm ${displayMarkerPath}` ].filter((line) => Boolean(line)).join("\n"), "Gateway (macOS)"); } async function launchctlGetenv(name) { try { const result = await execFileAsync("/bin/launchctl", ["getenv", name], { encoding: "utf8" }); const value = String(result.stdout ?? "").trim(); return value.length > 0 ? value : void 0; } catch { return; } } function hasConfigGatewayCreds(cfg) { const localToken = typeof cfg.gateway?.auth?.token === "string" ? cfg.gateway?.auth?.token.trim() : ""; const localPassword = typeof cfg.gateway?.auth?.password === "string" ? cfg.gateway?.auth?.password.trim() : ""; const remoteToken = typeof cfg.gateway?.remote?.token === "string" ? cfg.gateway?.remote?.token.trim() : ""; const remotePassword = typeof cfg.gateway?.remote?.password === "string" ? cfg.gateway?.remote?.password.trim() : ""; return Boolean(localToken || localPassword || remoteToken || remotePassword); } async function noteMacLaunchctlGatewayEnvOverrides(cfg, deps) { if ((deps?.platform ?? process.platform) !== "darwin") return; if (!hasConfigGatewayCreds(cfg)) return; const getenv = deps?.getenv ?? launchctlGetenv; const deprecatedLaunchctlEntries = [["CLAWDBOT_GATEWAY_TOKEN", await getenv("CLAWDBOT_GATEWAY_TOKEN")], ["CLAWDBOT_GATEWAY_PASSWORD", await getenv("CLAWDBOT_GATEWAY_PASSWORD")]].filter((entry) => Boolean(entry[1]?.trim())); if (deprecatedLaunchctlEntries.length > 0) { const lines = ["- Deprecated launchctl environment variables detected (ignored).", ...deprecatedLaunchctlEntries.map(([key]) => `- \`${key}\` is set; use \`OPENCLAW_${key.slice(key.indexOf("_") + 1)}\` instead.`)]; (deps?.noteFn ?? note$1)(lines.join("\n"), "Gateway (macOS)"); } const tokenEntries = [["OPENCLAW_GATEWAY_TOKEN", await getenv("OPENCLAW_GATEWAY_TOKEN")]]; const passwordEntries = [["OPENCLAW_GATEWAY_PASSWORD", await getenv("OPENCLAW_GATEWAY_PASSWORD")]]; const tokenEntry = tokenEntries.find(([, value]) => value?.trim()); const passwordEntry = passwordEntries.find(([, value]) => value?.trim()); const envToken = tokenEntry?.[1]?.trim() ?? ""; const envPassword = passwordEntry?.[1]?.trim() ?? ""; const envTokenKey = tokenEntry?.[0]; const envPasswordKey = passwordEntry?.[0]; if (!envToken && !envPassword) return; const lines = [ "- launchctl environment overrides detected (can cause confusing unauthorized errors).", envToken && envTokenKey ? `- \`${envTokenKey}\` is set; it overrides config tokens.` : void 0, envPassword ? `- \`${envPasswordKey ?? "OPENCLAW_GATEWAY_PASSWORD"}\` is set; it overrides config passwords.` : void 0, "- Clear overrides and restart the app/gateway:", envTokenKey ? ` launchctl unsetenv ${envTokenKey}` : void 0, envPasswordKey ? ` launchctl unsetenv ${envPasswordKey}` : void 0 ].filter((line) => Boolean(line)); (deps?.noteFn ?? note$1)(lines.join("\n"), "Gateway (macOS)"); } function noteDeprecatedLegacyEnvVars(env = process.env, deps) { const entries = Object.entries(env).filter(([key, value]) => key.startsWith("CLAWDBOT_") && value?.trim()).map(([key]) => key); if (entries.length === 0) return; const lines = [ "- Deprecated legacy environment variables detected (ignored).", "- Use OPENCLAW_* equivalents instead:", ...entries.map((key) => { return ` ${key} -> OPENCLAW_${key.slice(key.indexOf("_") + 1)}`; }) ]; (deps?.noteFn ?? note$1)(lines.join("\n"), "Environment"); } //#endregion //#region src/commands/doctor-prompter.ts function createDoctorPrompter(params) { const yes = params.options.yes === true; const requestedNonInteractive = params.options.nonInteractive === true; const shouldRepair = params.options.repair === true || yes; const shouldForce = params.options.force === true; const isTty = Boolean(process.stdin.isTTY); const nonInteractive = requestedNonInteractive || !isTty && !yes; const canPrompt = isTty && !yes && !nonInteractive; const confirmDefault = async (p) => { if (nonInteractive) return false; if (shouldRepair) return true; if (!canPrompt) return Boolean(p.initialValue ?? false); return guardCancel(await confirm({ ...p, message: stylePromptMessage(p.message) }), params.runtime); }; return { confirm: confirmDefault, confirmRepair: async (p) => { if (nonInteractive) return false; return confirmDefault(p); }, confirmAggressive: async (p) => { if (nonInteractive) return false; if (shouldRepair && shouldForce) return true; if (shouldRepair && !shouldForce) return false; if (!canPrompt) return Boolean(p.initialValue ?? false); return guardCancel(await confirm({ ...p, message: stylePromptMessage(p.message) }), params.runtime); }, confirmSkipInNonInteractive: async (p) => { if (nonInteractive) return false; if (shouldRepair) return true; return confirmDefault(p); }, select: async (p, fallback) => { if (!canPrompt || shouldRepair) return fallback; return guardCancel(await select({ ...p, message: stylePromptMessage(p.message), options: p.options.map((opt) => opt.hint === void 0 ? opt : { ...opt, hint: stylePromptHint(opt.hint) }) }), params.runtime); }, shouldRepair, shouldForce }; } //#endregion //#region src/commands/doctor-sandbox.ts function resolveSandboxScript(scriptRel) { const candidates = /* @__PURE__ */ new Set(); candidates.add(process.cwd()); const argv1 = process.argv[1]; if (argv1) { const normalized = path.resolve(argv1); candidates.add(path.resolve(path.dirname(normalized), "..")); candidates.add(path.resolve(path.dirname(normalized))); } for (const root of candidates) { const scriptPath = path.join(root, scriptRel); if (fs.existsSync(scriptPath)) return { scriptPath, cwd: root }; } return null; } async function runSandboxScript(scriptRel, runtime) { const script = resolveSandboxScript(scriptRel); if (!script) { note$1(`Unable to locate ${scriptRel}. Run it from the repo root.`, "Sandbox"); return false; } runtime.log(`Running ${scriptRel}...`); const result = await runCommandWithTimeout(["bash", script.scriptPath], { timeoutMs: 1200 * 1e3, cwd: script.cwd }); if (result.code !== 0) { runtime.error(`Failed running ${scriptRel}: ${result.stderr.trim() || result.stdout.trim() || "unknown error"}`); return false; } runtime.log(`Completed ${scriptRel}.`); return true; } async function isDockerAvailable() { try { await runExec("docker", [ "version", "--format", "{{.Server.Version}}" ], { timeoutMs: 5e3 }); return true; } catch { return false; } } async function dockerImageExists(image) { try { await runExec("docker", [ "image", "inspect", image ], { timeoutMs: 5e3 }); return true; } catch (error) { const stderr = error?.stderr || error?.message || ""; if (String(stderr).includes("No such image")) return false; throw error; } } function resolveSandboxDockerImage(cfg) { const image = cfg.agents?.defaults?.sandbox?.docker?.image?.trim(); return image ? image : DEFAULT_SANDBOX_IMAGE; } function resolveSandboxBrowserImage(cfg) { const image = cfg.agents?.defaults?.sandbox?.browser?.image?.trim(); return image ? image : DEFAULT_SANDBOX_BROWSER_IMAGE; } function updateSandboxDockerImage(cfg, image) { return { ...cfg, agents: { ...cfg.agents, defaults: { ...cfg.agents?.defaults, sandbox: { ...cfg.agents?.defaults?.sandbox, docker: { ...cfg.agents?.defaults?.sandbox?.docker, image } } } } }; } function updateSandboxBrowserImage(cfg, image) { return { ...cfg, agents: { ...cfg.agents, defaults: { ...cfg.agents?.defaults, sandbox: { ...cfg.agents?.defaults?.sandbox, browser: { ...cfg.agents?.defaults?.sandbox?.browser, image } } } } }; } async function handleMissingSandboxImage(params, runtime, prompter) { if (await dockerImageExists(params.image)) return; const buildHint = params.buildScript ? `Build it with ${params.buildScript}.` : "Build or pull it first."; note$1(`Sandbox ${params.kind} image missing: ${params.image}. ${buildHint}`, "Sandbox"); let built = false; if (params.buildScript) { if (await prompter.confirmSkipInNonInteractive({ message: `Build ${params.kind} sandbox image now?`, initialValue: true })) built = await runSandboxScript(params.buildScript, runtime); } if (built) return; } async function maybeRepairSandboxImages(cfg, runtime, prompter) { const sandbox = cfg.agents?.defaults?.sandbox; const mode = sandbox?.mode ?? "off"; if (!sandbox || mode === "off") return cfg; if (!await isDockerAvailable()) { note$1("Docker not available; skipping sandbox image checks.", "Sandbox"); return cfg; } let next = cfg; const changes = []; const dockerImage = resolveSandboxDockerImage(cfg); await handleMissingSandboxImage({ kind: "base", image: dockerImage, buildScript: dockerImage === DEFAULT_SANDBOX_COMMON_IMAGE ? "scripts/sandbox-common-setup.sh" : dockerImage === DEFAULT_SANDBOX_IMAGE ? "scripts/sandbox-setup.sh" : void 0, updateConfig: (image) => { next = updateSandboxDockerImage(next, image); changes.push(`Updated agents.defaults.sandbox.docker.image → ${image}`); } }, runtime, prompter); if (sandbox.browser?.enabled) await handleMissingSandboxImage({ kind: "browser", image: resolveSandboxBrowserImage(cfg), buildScript: "scripts/sandbox-browser-setup.sh", updateConfig: (image) => { next = updateSandboxBrowserImage(next, image); changes.push(`Updated agents.defaults.sandbox.browser.image → ${image}`); } }, runtime, prompter); if (changes.length > 0) note$1(changes.join("\n"), "Doctor changes"); return next; } function noteSandboxScopeWarnings(cfg) { const globalSandbox = cfg.agents?.defaults?.sandbox; const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : []; const warnings = []; for (const agent of agents) { const agentId = agent.id; const agentSandbox = agent.sandbox; if (!agentSandbox) continue; if (resolveSandboxScope({ scope: agentSandbox.scope ?? globalSandbox?.scope, perSession: agentSandbox.perSession ?? globalSandbox?.perSession }) !== "shared") continue; const overrides = []; if (agentSandbox.docker && Object.keys(agentSandbox.docker).length > 0) overrides.push("docker"); if (agentSandbox.browser && Object.keys(agentSandbox.browser).length > 0) overrides.push("browser"); if (agentSandbox.prune && Object.keys(agentSandbox.prune).length > 0) overrides.push("prune"); if (overrides.length === 0) continue; warnings.push([`- agents.list (id "${agentId}") sandbox ${overrides.join("/")} overrides ignored.`, ` scope resolves to "shared".`].join("\n")); } if (warnings.length > 0) note$1(warnings.join("\n"), "Sandbox"); } //#endregion //#region src/commands/doctor-security.ts async function noteSecurityWarnings(cfg) { const warnings = []; const auditHint = `- Run: ${formatCliCommand("openclaw security audit --deep")}`; const gatewayBind = cfg.gateway?.bind ?? "loopback"; const customBindHost = cfg.gateway?.customBindHost?.trim(); const bindMode = [ "auto", "lan", "loopback", "custom", "tailnet" ].includes(gatewayBind) ? gatewayBind : void 0; const resolvedBindHost = bindMode ? await resolveGatewayBindHost(bindMode, customBindHost) : "0.0.0.0"; const isExposed = !isLoopbackHost(resolvedBindHost); const resolvedAuth = resolveGatewayAuth({ authConfig: cfg.gateway?.auth, env: process.env, tailscaleMode: cfg.gateway?.tailscale?.mode ?? "off" }); const authToken = resolvedAuth.token?.trim() ?? ""; const authPassword = resolvedAuth.password?.trim() ?? ""; const hasToken = authToken.length > 0; const hasPassword = authPassword.length > 0; const hasSharedSecret = resolvedAuth.mode === "token" && hasToken || resolvedAuth.mode === "password" && hasPassword; const bindDescriptor = `"${gatewayBind}" (${resolvedBindHost})`; if (isExposed) if (!hasSharedSecret) { const authFixLines = resolvedAuth.mode === "password" ? [` Fix: ${formatCliCommand("openclaw configure")} to set a password`, ` Or switch to token: ${formatCliCommand("openclaw config set gateway.auth.mode token")}`] : [` Fix: ${formatCliCommand("openclaw doctor --fix")} to generate a token`, ` Or set token directly: ${formatCliCommand("openclaw config set gateway.auth.mode token")}`]; warnings.push(`- CRITICAL: Gateway bound to ${bindDescriptor} without authentication.`, ` Anyone on your network (or internet if port-forwarded) can fully control your agent.`, ` Fix: ${formatCliCommand("openclaw config set gateway.bind loopback")}`, ...authFixLines); } else warnings.push(`- WARNING: Gateway bound to ${bindDescriptor} (network-accessible).`, ` Ensure your auth credentials are strong and not exposed.`); const warnDmPolicy = async (params) => { const dmPolicy = params.dmPolicy; const policyPath = params.policyPath ?? `${params.allowFromPath}policy`; const { hasWildcard, allowCount, isMultiUserDm } = await resolveDmAllowState({ provider: params.provider, allowFrom: params.allowFrom, normalizeEntry: params.normalizeEntry }); const dmScope = cfg.session?.dmScope ?? "main"; if (dmPolicy === "open") { const allowFromPath = `${params.allowFromPath}allowFrom`; warnings.push(`- ${params.label} DMs: OPEN (${policyPath}="open"). Anyone can DM it.`); if (!hasWildcard) warnings.push(`- ${params.label} DMs: config invalid — "open" requires ${allowFromPath} to include "*".`); } if (dmPolicy === "disabled") { warnings.push(`- ${params.label} DMs: disabled (${policyPath}="disabled").`); return; } if (dmPolicy !== "open" && allowCount === 0) { warnings.push(`- ${params.label} DMs: locked (${policyPath}="${dmPolicy}") with no allowlist; unknown senders will be blocked / get a pairing code.`); warnings.push(` ${params.approveHint}`); } if (dmScope === "main" && isMultiUserDm) warnings.push(`- ${params.label} DMs: multiple senders share the main session; run: ` + formatCliCommand("openclaw config set session.dmScope \"per-channel-peer\"") + " (or \"per-account-channel-peer\" for multi-account channels) to isolate sessions."); }; for (const plugin of listChannelPlugins()) { if (!plugin.security) continue; const defaultAccountId = resolveChannelDefaultAccountId({ plugin, cfg, accountIds: plugin.config.listAccountIds(cfg) }); const account = plugin.config.resolveAccount(cfg, defaultAccountId); if (!(plugin.config.isEnabled ? plugin.config.isEnabled(account, cfg) : true)) continue; if (!(plugin.config.isConfigured ? await plugin.config.isConfigured(account, cfg) : true)) continue; const dmPolicy = plugin.security.resolveDmPolicy?.({ cfg, accountId: defaultAccountId, account }); if (dmPolicy) await warnDmPolicy({ label: plugin.meta.label ?? plugin.id, provider: plugin.id, dmPolicy: dmPolicy.policy, allowFrom: dmPolicy.allowFrom, policyPath: dmPolicy.policyPath, allowFromPath: dmPolicy.allowFromPath, approveHint: dmPolicy.approveHint, normalizeEntry: dmPolicy.normalizeEntry }); if (plugin.security.collectWarnings) { const extra = await plugin.security.collectWarnings({ cfg, accountId: defaultAccountId, account }); if (extra?.length) warnings.push(...extra); } } const lines = warnings.length > 0 ? warnings : ["- No channel security warnings detected."]; lines.push(auditHint); note$1(lines.join("\n"), "Security"); } //#endregion //#region src/commands/doctor-session-locks.ts const DEFAULT_STALE_MS = 1800 * 1e3; function formatAge(ageMs) { if (ageMs === null) return "unknown"; const seconds = Math.floor(ageMs / 1e3); if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; if (minutes < 60) return `${minutes}m${remainingSeconds}s`; return `${Math.floor(minutes / 60)}h${minutes % 60}m`; } function formatLockLine(lock) { const pidStatus = lock.pid === null ? "pid=missing" : `pid=${lock.pid} (${lock.pidAlive ? "alive" : "dead"})`; const ageStatus = `age=${formatAge(lock.ageMs)}`; const staleStatus = lock.stale ? `stale=yes (${lock.staleReasons.join(", ") || "unknown"})` : "stale=no"; const removedStatus = lock.removed ? " [removed]" : ""; return `- ${shortenHomePath(lock.lockPath)} ${pidStatus} ${ageStatus} ${staleStatus}${removedStatus}`; } async function noteSessionLockHealth(params) { const s