@gguf/claw
Version:
WhatsApp gateway CLI (Baileys web) with Pi RPC agent
1,333 lines (1,325 loc) • 99.8 kB
JavaScript
import { F as CONFIG_PATH, G as resolveIsNixMode, J as resolveOAuthDir, K as resolveLegacyStateDirs, M as getResolvedLoggerSettings, W as resolveGatewayPort, X as resolveStateDir, n as isTruthyEnvValue, p as defaultRuntime, q as resolveNewStateDir } from "./entry.js";
import { E as getModelRefStatus, F as resolveHooksGmailModel, N as resolveConfiguredModelRef, _ as ensureAuthProfileStore, _t as DEFAULT_PROVIDER, bt as CODEX_CLI_PROFILE_ID, f as repairOAuthProfileIdMismatch, gt as DEFAULT_MODEL, o as resolveProfileUnusableUntilForDisplay, s as resolveApiKeyForProfile, y as updateAuthProfileStoreWithLock, yt as CLAUDE_CLI_PROFILE_ID } from "./auth-profiles-CYBuGiBb.js";
import { t as formatCliCommand } from "./command-format-ayFsmwwz.js";
import { c as normalizeAgentId, i as buildAgentMainSessionKey, r as DEFAULT_MAIN_KEY, t as DEFAULT_ACCOUNT_ID } from "./session-key-CZkcvAtx.js";
import { _ as sleep, f as resolveHomeDir$1, g as shortenHomePath } from "./utils-DX85MiPR.js";
import { n as runExec, t as runCommandWithTimeout } from "./exec-B8JKbXKW.js";
import { t as resolveOpenClawPackageRoot } from "./openclaw-root-9ILYSmJ9.js";
import { c as resolveDefaultAgentId, d as DEFAULT_AGENTS_FILENAME, s as resolveAgentWorkspaceDir } from "./agent-scope-C9VjJXEK.js";
import { c as writeConfigFile, n as migrateLegacyConfig, o as readConfigFileSnapshot, u as OpenClawSchema } from "./config-CKLedg5Y.js";
import { f as inspectPortUsage, m as formatPortDiagnostics } from "./errors-CZ9opC6L.js";
import { a as resolveGatewayBindHost, n as isLoopbackHost } from "./net-CWMMy37F.js";
import { i as resolveGatewayAuth } from "./auth-DksjO6WG.js";
import { n as callGateway, t as buildGatewayConnectionDetails } from "./call-90HgQQ8o.js";
import { t as applyPluginAutoEnable } from "./plugin-auto-enable-DyW8lHTT.js";
import { n as listChannelPlugins } from "./plugins-BUPpq5aS.js";
import { Kt as loadModelCatalog, _ as randomToken, a as applyWizardMetadata, h as printWizardHeader, t as loadOpenClawPlugins, u as guardCancel } from "./loader-_Pj-TZS2.js";
import { n as stylePromptMessage, r as stylePromptTitle, t as stylePromptHint } from "./prompt-style-Dc0C5HC9.js";
import { t as note$1 } from "./note-Ci08TSbV.js";
import { t as resolveChannelDefaultAccountId } from "./helpers-D66_XoIz.js";
import { a as resolveSessionTranscriptsDirForAgent, n as resolveSessionFilePath, o as resolveStorePath } from "./paths-CTg8F3AE.js";
import { I as resolveMainSessionKey, N as canonicalizeMainSessionAlias, W as resolveSandboxScope, d as loadSessionStore, et as DEFAULT_SANDBOX_BROWSER_IMAGE, m as saveSessionStore, nt as DEFAULT_SANDBOX_IMAGE, tt as DEFAULT_SANDBOX_COMMON_IMAGE } from "./sandbox-DuqLKN5J.js";
import { n as isWSLEnv, t as isWSL } from "./wsl-ATjkMwMA.js";
import { d as resolveGatewaySystemdServiceName, f as resolveGatewayWindowsTaskName, l as resolveGatewayLaunchAgentLabel, m as resolveNodeLaunchAgentLabel } from "./constants-D1op9uGI.js";
import { i as readChannelAllowFromStore } from "./pairing-store-DTfv_FGA.js";
import { t as collectChannelStatusIssues } from "./channels-status-issues-CJ8PJgDc.js";
import { a as gatewayInstallErrorHint, d as renderSystemNodeWarning, i as buildGatewayInstallPlan, n as GATEWAY_DAEMON_RUNTIME_OPTIONS, p as resolveSystemNodeInfo, t as DEFAULT_GATEWAY_DAEMON_RUNTIME } from "./daemon-runtime-BCn_QIHK.js";
import { a as repairLaunchAgentBootstrap, i as launchAgentPlistExists, n as isLaunchAgentListed, o as resolveGatewayLogPaths, r as isLaunchAgentLoaded, t as resolveGatewayService } from "./service-_JwSmGSn.js";
import { r as isSystemdUserServiceAvailable } from "./systemd-8sIc6isV.js";
import { n as renderSystemdUnavailableHints, t as isSystemdUnavailableDetail } from "./systemd-hints-Wim4Bq6j.js";
import { a as renderGatewayServiceCleanupHints, i as findExtraGatewayServices, n as auditGatewayServiceConfig, o as readLastGatewayErrorLine, r as needsNodeRuntimeMigration, t as SERVICE_AUDIT_CODES } from "./service-audit-DDX1kO3k.js";
import { i as resolveControlUiDistIndexPathForRoot, l as healthCommand, r as resolveControlUiDistIndexHealth, t as formatHealthCheckFailure } from "./health-format-ND2rUbQO.js";
import { f as doctorShellCompletion, t as runGatewayUpdate } from "./update-runner-2i8_mIG5.js";
import { n as logConfigUpdated } from "./logging-Cc7m6PTv.js";
import { t as buildWorkspaceSkillStatus } from "./skills-status-DtXrj3fy.js";
import { n as buildAuthHealthSummary, r as formatRemainingShort, t as DEFAULT_OAUTH_WARN_MS } from "./auth-health-C4bElkgf.js";
import { t as ensureSystemdUserLingerInteractive } from "./systemd-linger-SsSOsJST.js";
import { execFile } from "node:child_process";
import path from "node:path";
import os from "node:os";
import fs from "node:fs";
import JSON5 from "json5";
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-legacy-config.ts
function normalizeLegacyConfigValues(cfg) {
const changes = [];
let next = cfg;
const legacyAckReaction = cfg.messages?.ackReaction?.trim();
const hasWhatsAppConfig = cfg.channels?.whatsapp !== void 0;
if (legacyAckReaction && hasWhatsAppConfig) {
if (!(cfg.channels?.whatsapp?.ackReaction !== void 0)) {
const legacyScope = cfg.messages?.ackReactionScope ?? "group-mentions";
let direct = true;
let group = "mentions";
if (legacyScope === "all") {
direct = true;
group = "always";
} else if (legacyScope === "direct") {
direct = true;
group = "never";
} else if (legacyScope === "group-all") {
direct = false;
group = "always";
} else if (legacyScope === "group-mentions") {
direct = false;
group = "mentions";
}
next = {
...next,
channels: {
...next.channels,
whatsapp: {
...next.channels?.whatsapp,
ackReaction: {
emoji: legacyAckReaction,
direct,
group
}
}
}
};
changes.push(`Copied messages.ackReaction → channels.whatsapp.ackReaction (scope: ${legacyScope}).`);
}
}
return {
config: next,
changes
};
}
//#endregion
//#region src/infra/state-migrations.fs.ts
function safeReadDir(dir) {
try {
return fs.readdirSync(dir, { withFileTypes: true });
} catch {
return [];
}
}
function existsDir$1(dir) {
try {
return fs.existsSync(dir) && fs.statSync(dir).isDirectory();
} catch {
return false;
}
}
function ensureDir$1(dir) {
fs.mkdirSync(dir, { recursive: true });
}
function fileExists(p) {
try {
return fs.existsSync(p) && fs.statSync(p).isFile();
} catch {
return false;
}
}
function isLegacyWhatsAppAuthFile(name) {
if (name === "creds.json" || name === "creds.json.bak") return true;
if (!name.endsWith(".json")) return false;
return /^(app-state-sync|session|sender-key|pre-key)-/.test(name);
}
function readSessionStoreJson5(storePath) {
try {
const raw = fs.readFileSync(storePath, "utf-8");
const parsed = JSON5.parse(raw);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return {
store: parsed,
ok: true
};
} catch {}
return {
store: {},
ok: false
};
}
//#endregion
//#region src/infra/state-migrations.ts
let autoMigrateStateDirChecked = false;
function isSurfaceGroupKey(key) {
return key.includes(":group:") || key.includes(":channel:");
}
function isLegacyGroupKey(key) {
const trimmed = key.trim();
if (!trimmed) return false;
if (trimmed.startsWith("group:")) return true;
const lower = trimmed.toLowerCase();
if (!lower.includes("@g.us")) return false;
if (!trimmed.includes(":")) return true;
if (lower.startsWith("whatsapp:") && !trimmed.includes(":group:")) return true;
return false;
}
function canonicalizeSessionKeyForAgent(params) {
const agentId = normalizeAgentId(params.agentId);
const raw = params.key.trim();
if (!raw) return raw;
if (raw.toLowerCase() === "global" || raw.toLowerCase() === "unknown") return raw.toLowerCase();
const canonicalMain = canonicalizeMainSessionAlias({
cfg: { session: {
scope: params.scope,
mainKey: params.mainKey
} },
agentId,
sessionKey: raw
});
if (canonicalMain !== raw) return canonicalMain.toLowerCase();
if (raw.toLowerCase().startsWith("agent:")) return raw.toLowerCase();
if (raw.toLowerCase().startsWith("subagent:")) return `agent:${agentId}:subagent:${raw.slice(9)}`.toLowerCase();
if (raw.startsWith("group:")) {
const id = raw.slice(6).trim();
if (!id) return raw;
return `agent:${agentId}:${id.toLowerCase().includes("@g.us") ? "whatsapp" : "unknown"}:group:${id}`.toLowerCase();
}
if (!raw.includes(":") && raw.toLowerCase().includes("@g.us")) return `agent:${agentId}:whatsapp:group:${raw}`.toLowerCase();
if (raw.toLowerCase().startsWith("whatsapp:") && raw.toLowerCase().includes("@g.us")) {
const cleaned = raw.slice(9).trim().replace(/^group:/i, "").trim();
if (cleaned && !isSurfaceGroupKey(raw)) return `agent:${agentId}:whatsapp:group:${cleaned}`.toLowerCase();
}
if (isSurfaceGroupKey(raw)) return `agent:${agentId}:${raw}`.toLowerCase();
return `agent:${agentId}:${raw}`.toLowerCase();
}
function pickLatestLegacyDirectEntry(store) {
let best = null;
let bestUpdated = -1;
for (const [key, entry] of Object.entries(store)) {
if (!entry || typeof entry !== "object") continue;
const normalized = key.trim();
if (!normalized) continue;
if (normalized === "global") continue;
if (normalized.startsWith("agent:")) continue;
if (normalized.toLowerCase().startsWith("subagent:")) continue;
if (isLegacyGroupKey(normalized) || isSurfaceGroupKey(normalized)) continue;
const updatedAt = typeof entry.updatedAt === "number" ? entry.updatedAt : 0;
if (updatedAt > bestUpdated) {
bestUpdated = updatedAt;
best = entry;
}
}
return best;
}
function normalizeSessionEntry(entry) {
const sessionId = typeof entry.sessionId === "string" ? entry.sessionId : null;
if (!sessionId) return null;
const updatedAt = typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt) ? entry.updatedAt : Date.now();
const normalized = {
...entry,
sessionId,
updatedAt
};
const rec = normalized;
if (typeof rec.groupChannel !== "string" && typeof rec.room === "string") rec.groupChannel = rec.room;
delete rec.room;
return normalized;
}
function resolveUpdatedAt(entry) {
return typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt) ? entry.updatedAt : 0;
}
function mergeSessionEntry(params) {
if (!params.existing) return params.incoming;
const existingUpdated = resolveUpdatedAt(params.existing);
const incomingUpdated = resolveUpdatedAt(params.incoming);
if (incomingUpdated > existingUpdated) return params.incoming;
if (incomingUpdated < existingUpdated) return params.existing;
return params.preferIncomingOnTie ? params.incoming : params.existing;
}
function canonicalizeSessionStore(params) {
const canonical = {};
const meta = /* @__PURE__ */ new Map();
const legacyKeys = [];
for (const [key, entry] of Object.entries(params.store)) {
if (!entry || typeof entry !== "object") continue;
const canonicalKey = canonicalizeSessionKeyForAgent({
key,
agentId: params.agentId,
mainKey: params.mainKey,
scope: params.scope
});
const isCanonical = canonicalKey === key;
if (!isCanonical) legacyKeys.push(key);
const existing = canonical[canonicalKey];
if (!existing) {
canonical[canonicalKey] = entry;
meta.set(canonicalKey, {
isCanonical,
updatedAt: resolveUpdatedAt(entry)
});
continue;
}
const existingMeta = meta.get(canonicalKey);
const incomingUpdated = resolveUpdatedAt(entry);
const existingUpdated = existingMeta?.updatedAt ?? resolveUpdatedAt(existing);
if (incomingUpdated > existingUpdated) {
canonical[canonicalKey] = entry;
meta.set(canonicalKey, {
isCanonical,
updatedAt: incomingUpdated
});
continue;
}
if (incomingUpdated < existingUpdated) continue;
if (existingMeta?.isCanonical && !isCanonical) continue;
if (!existingMeta?.isCanonical && isCanonical) {
canonical[canonicalKey] = entry;
meta.set(canonicalKey, {
isCanonical,
updatedAt: incomingUpdated
});
continue;
}
}
return {
store: canonical,
legacyKeys
};
}
function listLegacySessionKeys(params) {
const legacy = [];
for (const key of Object.keys(params.store)) if (canonicalizeSessionKeyForAgent({
key,
agentId: params.agentId,
mainKey: params.mainKey,
scope: params.scope
}) !== key) legacy.push(key);
return legacy;
}
function emptyDirOrMissing(dir) {
if (!existsDir$1(dir)) return true;
return safeReadDir(dir).length === 0;
}
function removeDirIfEmpty(dir) {
if (!existsDir$1(dir)) return;
if (!emptyDirOrMissing(dir)) return;
try {
fs.rmdirSync(dir);
} catch {}
}
function resolveSymlinkTarget(linkPath) {
try {
const target = fs.readlinkSync(linkPath);
return path.resolve(path.dirname(linkPath), target);
} catch {
return null;
}
}
function formatStateDirMigration(legacyDir, targetDir) {
return `State dir: ${legacyDir} → ${targetDir} (legacy path now symlinked)`;
}
function isDirPath(filePath) {
try {
return fs.statSync(filePath).isDirectory();
} catch {
return false;
}
}
async function autoMigrateLegacyStateDir(params) {
if (autoMigrateStateDirChecked) return {
migrated: false,
skipped: true,
changes: [],
warnings: []
};
autoMigrateStateDirChecked = true;
if ((params.env ?? process.env).OPENCLAW_STATE_DIR?.trim()) return {
migrated: false,
skipped: true,
changes: [],
warnings: []
};
const homedir = params.homedir ?? os.homedir;
const targetDir = resolveNewStateDir(homedir);
const legacyDirs = resolveLegacyStateDirs(homedir);
let legacyDir = legacyDirs.find((dir) => {
try {
return fs.existsSync(dir);
} catch {
return false;
}
});
const warnings = [];
const changes = [];
let legacyStat = null;
try {
legacyStat = legacyDir ? fs.lstatSync(legacyDir) : null;
} catch {
legacyStat = null;
}
if (!legacyStat) return {
migrated: false,
skipped: false,
changes,
warnings
};
if (!legacyStat.isDirectory() && !legacyStat.isSymbolicLink()) {
warnings.push(`Legacy state path is not a directory: ${legacyDir}`);
return {
migrated: false,
skipped: false,
changes,
warnings
};
}
let symlinkDepth = 0;
while (legacyStat.isSymbolicLink()) {
const legacyTarget = legacyDir ? resolveSymlinkTarget(legacyDir) : null;
if (!legacyTarget) {
warnings.push(`Legacy state dir is a symlink (${legacyDir ?? "unknown"}); could not resolve target.`);
return {
migrated: false,
skipped: false,
changes,
warnings
};
}
if (path.resolve(legacyTarget) === path.resolve(targetDir)) return {
migrated: false,
skipped: false,
changes,
warnings
};
if (legacyDirs.some((dir) => path.resolve(dir) === path.resolve(legacyTarget))) {
legacyDir = legacyTarget;
try {
legacyStat = fs.lstatSync(legacyDir);
} catch {
legacyStat = null;
}
if (!legacyStat) {
warnings.push(`Legacy state dir missing after symlink resolution: ${legacyDir}`);
return {
migrated: false,
skipped: false,
changes,
warnings
};
}
if (!legacyStat.isDirectory() && !legacyStat.isSymbolicLink()) {
warnings.push(`Legacy state path is not a directory: ${legacyDir}`);
return {
migrated: false,
skipped: false,
changes,
warnings
};
}
symlinkDepth += 1;
if (symlinkDepth > 2) {
warnings.push(`Legacy state dir symlink chain too deep: ${legacyDir}`);
return {
migrated: false,
skipped: false,
changes,
warnings
};
}
continue;
}
warnings.push(`Legacy state dir is a symlink (${legacyDir ?? "unknown"} → ${legacyTarget}); skipping auto-migration.`);
return {
migrated: false,
skipped: false,
changes,
warnings
};
}
if (isDirPath(targetDir)) {
warnings.push(`State dir migration skipped: target already exists (${targetDir}). Remove or merge manually.`);
return {
migrated: false,
skipped: false,
changes,
warnings
};
}
try {
if (!legacyDir) throw new Error("Legacy state dir not found");
fs.renameSync(legacyDir, targetDir);
} catch (err) {
warnings.push(`Failed to move legacy state dir (${legacyDir ?? "unknown"} → ${targetDir}): ${String(err)}`);
return {
migrated: false,
skipped: false,
changes,
warnings
};
}
try {
if (!legacyDir) throw new Error("Legacy state dir not found");
fs.symlinkSync(targetDir, legacyDir, "dir");
changes.push(formatStateDirMigration(legacyDir, targetDir));
} catch (err) {
try {
if (process.platform === "win32") {
if (!legacyDir) throw new Error("Legacy state dir not found", { cause: err });
fs.symlinkSync(targetDir, legacyDir, "junction");
changes.push(formatStateDirMigration(legacyDir, targetDir));
} else throw err;
} catch (fallbackErr) {
try {
if (!legacyDir) throw new Error("Legacy state dir not found", { cause: fallbackErr });
fs.renameSync(targetDir, legacyDir);
warnings.push(`State dir migration rolled back (failed to link legacy path): ${String(fallbackErr)}`);
return {
migrated: false,
skipped: false,
changes: [],
warnings
};
} catch (rollbackErr) {
warnings.push(`State dir moved but failed to link legacy path (${legacyDir ?? "unknown"} → ${targetDir}): ${String(fallbackErr)}`);
warnings.push(`Rollback failed; set OPENCLAW_STATE_DIR=${targetDir} to avoid split state: ${String(rollbackErr)}`);
changes.push(`State dir: ${legacyDir ?? "unknown"} → ${targetDir}`);
}
}
}
return {
migrated: changes.length > 0,
skipped: false,
changes,
warnings
};
}
async function detectLegacyStateMigrations(params) {
const env = params.env ?? process.env;
const stateDir = resolveStateDir(env, params.homedir ?? os.homedir);
const oauthDir = resolveOAuthDir(env, stateDir);
const targetAgentId = normalizeAgentId(resolveDefaultAgentId(params.cfg));
const rawMainKey = params.cfg.session?.mainKey;
const targetMainKey = typeof rawMainKey === "string" && rawMainKey.trim().length > 0 ? rawMainKey.trim() : DEFAULT_MAIN_KEY;
const targetScope = params.cfg.session?.scope;
const sessionsLegacyDir = path.join(stateDir, "sessions");
const sessionsLegacyStorePath = path.join(sessionsLegacyDir, "sessions.json");
const sessionsTargetDir = path.join(stateDir, "agents", targetAgentId, "sessions");
const sessionsTargetStorePath = path.join(sessionsTargetDir, "sessions.json");
const legacySessionEntries = safeReadDir(sessionsLegacyDir);
const hasLegacySessions = fileExists(sessionsLegacyStorePath) || legacySessionEntries.some((e) => e.isFile() && e.name.endsWith(".jsonl"));
const targetSessionParsed = fileExists(sessionsTargetStorePath) ? readSessionStoreJson5(sessionsTargetStorePath) : {
store: {},
ok: true
};
const legacyKeys = targetSessionParsed.ok ? listLegacySessionKeys({
store: targetSessionParsed.store,
agentId: targetAgentId,
mainKey: targetMainKey,
scope: targetScope
}) : [];
const legacyAgentDir = path.join(stateDir, "agent");
const targetAgentDir = path.join(stateDir, "agents", targetAgentId, "agent");
const hasLegacyAgentDir = existsDir$1(legacyAgentDir);
const targetWhatsAppAuthDir = path.join(oauthDir, "whatsapp", DEFAULT_ACCOUNT_ID);
const hasLegacyWhatsAppAuth = fileExists(path.join(oauthDir, "creds.json")) && !fileExists(path.join(targetWhatsAppAuthDir, "creds.json"));
const preview = [];
if (hasLegacySessions) preview.push(`- Sessions: ${sessionsLegacyDir} → ${sessionsTargetDir}`);
if (legacyKeys.length > 0) preview.push(`- Sessions: canonicalize legacy keys in ${sessionsTargetStorePath}`);
if (hasLegacyAgentDir) preview.push(`- Agent dir: ${legacyAgentDir} → ${targetAgentDir}`);
if (hasLegacyWhatsAppAuth) preview.push(`- WhatsApp auth: ${oauthDir} → ${targetWhatsAppAuthDir} (keep oauth.json)`);
return {
targetAgentId,
targetMainKey,
targetScope,
stateDir,
oauthDir,
sessions: {
legacyDir: sessionsLegacyDir,
legacyStorePath: sessionsLegacyStorePath,
targetDir: sessionsTargetDir,
targetStorePath: sessionsTargetStorePath,
hasLegacy: hasLegacySessions || legacyKeys.length > 0,
legacyKeys
},
agentDir: {
legacyDir: legacyAgentDir,
targetDir: targetAgentDir,
hasLegacy: hasLegacyAgentDir
},
whatsappAuth: {
legacyDir: oauthDir,
targetDir: targetWhatsAppAuthDir,
hasLegacy: hasLegacyWhatsAppAuth
},
preview
};
}
async function migrateLegacySessions(detected, now) {
const changes = [];
const warnings = [];
if (!detected.sessions.hasLegacy) return {
changes,
warnings
};
ensureDir$1(detected.sessions.targetDir);
const legacyParsed = fileExists(detected.sessions.legacyStorePath) ? readSessionStoreJson5(detected.sessions.legacyStorePath) : {
store: {},
ok: true
};
const targetParsed = fileExists(detected.sessions.targetStorePath) ? readSessionStoreJson5(detected.sessions.targetStorePath) : {
store: {},
ok: true
};
const legacyStore = legacyParsed.store;
const targetStore = targetParsed.store;
const canonicalizedTarget = canonicalizeSessionStore({
store: targetStore,
agentId: detected.targetAgentId,
mainKey: detected.targetMainKey,
scope: detected.targetScope
});
const canonicalizedLegacy = canonicalizeSessionStore({
store: legacyStore,
agentId: detected.targetAgentId,
mainKey: detected.targetMainKey,
scope: detected.targetScope
});
const merged = { ...canonicalizedTarget.store };
for (const [key, entry] of Object.entries(canonicalizedLegacy.store)) merged[key] = mergeSessionEntry({
existing: merged[key],
incoming: entry,
preferIncomingOnTie: false
});
const mainKey = buildAgentMainSessionKey({
agentId: detected.targetAgentId,
mainKey: detected.targetMainKey
});
if (!merged[mainKey]) {
const latest = pickLatestLegacyDirectEntry(legacyStore);
if (latest?.sessionId) {
merged[mainKey] = latest;
changes.push(`Migrated latest direct-chat session → ${mainKey}`);
}
}
if (!legacyParsed.ok) warnings.push(`Legacy sessions store unreadable; left in place at ${detected.sessions.legacyStorePath}`);
if ((legacyParsed.ok || targetParsed.ok) && (Object.keys(legacyStore).length > 0 || Object.keys(targetStore).length > 0)) {
const normalized = {};
for (const [key, entry] of Object.entries(merged)) {
const normalizedEntry = normalizeSessionEntry(entry);
if (!normalizedEntry) continue;
normalized[key] = normalizedEntry;
}
await saveSessionStore(detected.sessions.targetStorePath, normalized);
changes.push(`Merged sessions store → ${detected.sessions.targetStorePath}`);
if (canonicalizedTarget.legacyKeys.length > 0) changes.push(`Canonicalized ${canonicalizedTarget.legacyKeys.length} legacy session key(s)`);
}
const entries = safeReadDir(detected.sessions.legacyDir);
for (const entry of entries) {
if (!entry.isFile()) continue;
if (entry.name === "sessions.json") continue;
const from = path.join(detected.sessions.legacyDir, entry.name);
const to = path.join(detected.sessions.targetDir, entry.name);
if (fileExists(to)) continue;
try {
fs.renameSync(from, to);
changes.push(`Moved ${entry.name} → agents/${detected.targetAgentId}/sessions`);
} catch (err) {
warnings.push(`Failed moving ${from}: ${String(err)}`);
}
}
if (legacyParsed.ok) try {
if (fileExists(detected.sessions.legacyStorePath)) fs.rmSync(detected.sessions.legacyStorePath, { force: true });
} catch {}
removeDirIfEmpty(detected.sessions.legacyDir);
if (safeReadDir(detected.sessions.legacyDir).filter((e) => e.isFile()).length > 0) {
const backupDir = `${detected.sessions.legacyDir}.legacy-${now()}`;
try {
fs.renameSync(detected.sessions.legacyDir, backupDir);
warnings.push(`Left legacy sessions at ${backupDir}`);
} catch {}
}
return {
changes,
warnings
};
}
async function migrateLegacyAgentDir(detected, now) {
const changes = [];
const warnings = [];
if (!detected.agentDir.hasLegacy) return {
changes,
warnings
};
ensureDir$1(detected.agentDir.targetDir);
const entries = safeReadDir(detected.agentDir.legacyDir);
for (const entry of entries) {
const from = path.join(detected.agentDir.legacyDir, entry.name);
const to = path.join(detected.agentDir.targetDir, entry.name);
if (fs.existsSync(to)) continue;
try {
fs.renameSync(from, to);
changes.push(`Moved agent file ${entry.name} → agents/${detected.targetAgentId}/agent`);
} catch (err) {
warnings.push(`Failed moving ${from}: ${String(err)}`);
}
}
removeDirIfEmpty(detected.agentDir.legacyDir);
if (!emptyDirOrMissing(detected.agentDir.legacyDir)) {
const backupDir = path.join(detected.stateDir, "agents", detected.targetAgentId, `agent.legacy-${now()}`);
try {
fs.renameSync(detected.agentDir.legacyDir, backupDir);
warnings.push(`Left legacy agent dir at ${backupDir}`);
} catch (err) {
warnings.push(`Failed relocating legacy agent dir: ${String(err)}`);
}
}
return {
changes,
warnings
};
}
async function migrateLegacyWhatsAppAuth(detected) {
const changes = [];
const warnings = [];
if (!detected.whatsappAuth.hasLegacy) return {
changes,
warnings
};
ensureDir$1(detected.whatsappAuth.targetDir);
const entries = safeReadDir(detected.whatsappAuth.legacyDir);
for (const entry of entries) {
if (!entry.isFile()) continue;
if (entry.name === "oauth.json") continue;
if (!isLegacyWhatsAppAuthFile(entry.name)) continue;
const from = path.join(detected.whatsappAuth.legacyDir, entry.name);
const to = path.join(detected.whatsappAuth.targetDir, entry.name);
if (fileExists(to)) continue;
try {
fs.renameSync(from, to);
changes.push(`Moved WhatsApp auth ${entry.name} → whatsapp/default`);
} catch (err) {
warnings.push(`Failed moving ${from}: ${String(err)}`);
}
}
return {
changes,
warnings
};
}
async function runLegacyStateMigrations(params) {
const now = params.now ?? (() => Date.now());
const detected = params.detected;
const sessions = await migrateLegacySessions(detected, now);
const agentDir = await migrateLegacyAgentDir(detected, now);
const whatsappAuth = await migrateLegacyWhatsAppAuth(detected);
return {
changes: [
...sessions.changes,
...agentDir.changes,
...whatsappAuth.changes
],
warnings: [
...sessions.warnings,
...agentDir.warnings,
...whatsappAuth.warnings
]
};
}
//#endregion
//#region src/commands/doctor-config-flow.ts
function isRecord(value) {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function normalizeIssuePath(path) {
return path.filter((part) => typeof part !== "symbol");
}
function isUnrecognizedKeysIssue(issue) {
return issue.code === "unrecognized_keys";
}
function formatPath(parts) {
if (parts.length === 0) return "<root>";
let out = "";
for (const part of parts) {
if (typeof part === "number") {
out += `[${part}]`;
continue;
}
out = out ? `${out}.${part}` : part;
}
return out || "<root>";
}
function resolvePathTarget(root, path) {
let current = root;
for (const part of path) {
if (typeof part === "number") {
if (!Array.isArray(current)) return null;
if (part < 0 || part >= current.length) return null;
current = current[part];
continue;
}
if (!current || typeof current !== "object" || Array.isArray(current)) return null;
const record = current;
if (!(part in record)) return null;
current = record[part];
}
return current;
}
function stripUnknownConfigKeys(config) {
const parsed = OpenClawSchema.safeParse(config);
if (parsed.success) return {
config,
removed: []
};
const next = structuredClone(config);
const removed = [];
for (const issue of parsed.error.issues) {
if (!isUnrecognizedKeysIssue(issue)) continue;
const path = normalizeIssuePath(issue.path);
const target = resolvePathTarget(next, path);
if (!target || typeof target !== "object" || Array.isArray(target)) continue;
const record = target;
for (const key of issue.keys) {
if (typeof key !== "string") continue;
if (!(key in record)) continue;
delete record[key];
removed.push(formatPath([...path, key]));
}
}
return {
config: next,
removed
};
}
function noteOpencodeProviderOverrides(cfg) {
const providers = cfg.models?.providers;
if (!providers) return;
const overrides = [];
if (providers.opencode) overrides.push("opencode");
if (providers["opencode-zen"]) overrides.push("opencode-zen");
if (overrides.length === 0) return;
const lines = overrides.flatMap((id) => {
const providerEntry = providers[id];
const api = isRecord(providerEntry) && typeof providerEntry.api === "string" ? providerEntry.api : void 0;
return [`- models.providers.${id} is set; this overrides the built-in OpenCode Zen catalog.`, api ? `- models.providers.${id}.api=${api}` : null].filter((line) => Boolean(line));
});
lines.push("- Remove these entries to restore per-model API routing + costs (then re-run onboarding if needed).");
note$1(lines.join("\n"), "OpenCode Zen");
}
async function maybeMigrateLegacyConfig() {
const changes = [];
const home = resolveHomeDir$1();
if (!home) return changes;
const targetDir = path.join(home, ".openclaw");
const targetPath = path.join(targetDir, "openclaw.json");
try {
await fs$1.access(targetPath);
return changes;
} catch {}
const legacyCandidates = [
path.join(home, ".clawdbot", "clawdbot.json"),
path.join(home, ".moltbot", "moltbot.json"),
path.join(home, ".moldbot", "moldbot.json")
];
let legacyPath = null;
for (const candidate of legacyCandidates) try {
await fs$1.access(candidate);
legacyPath = candidate;
break;
} catch {}
if (!legacyPath) return changes;
await fs$1.mkdir(targetDir, { recursive: true });
try {
await fs$1.copyFile(legacyPath, targetPath, fs$1.constants.COPYFILE_EXCL);
changes.push(`Migrated legacy config: ${legacyPath} -> ${targetPath}`);
} catch {}
return changes;
}
async function loadAndMaybeMigrateDoctorConfig(params) {
const shouldRepair = params.options.repair === true || params.options.yes === true;
const stateDirResult = await autoMigrateLegacyStateDir({ env: process.env });
if (stateDirResult.changes.length > 0) note$1(stateDirResult.changes.map((entry) => `- ${entry}`).join("\n"), "Doctor changes");
if (stateDirResult.warnings.length > 0) note$1(stateDirResult.warnings.map((entry) => `- ${entry}`).join("\n"), "Doctor warnings");
const legacyConfigChanges = await maybeMigrateLegacyConfig();
if (legacyConfigChanges.length > 0) note$1(legacyConfigChanges.map((entry) => `- ${entry}`).join("\n"), "Doctor changes");
let snapshot = await readConfigFileSnapshot();
const baseCfg = snapshot.config ?? {};
let cfg = baseCfg;
let candidate = structuredClone(baseCfg);
let pendingChanges = false;
let shouldWriteConfig = false;
const fixHints = [];
if (snapshot.exists && !snapshot.valid && snapshot.legacyIssues.length === 0) note$1("Config invalid; doctor will run with best-effort config.", "Config");
const warnings = snapshot.warnings ?? [];
if (warnings.length > 0) note$1(warnings.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n"), "Config warnings");
if (snapshot.legacyIssues.length > 0) {
note$1(snapshot.legacyIssues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n"), "Legacy config keys detected");
const { config: migrated, changes } = migrateLegacyConfig(snapshot.parsed);
if (changes.length > 0) note$1(changes.join("\n"), "Doctor changes");
if (migrated) {
candidate = migrated;
pendingChanges = pendingChanges || changes.length > 0;
}
if (shouldRepair) {
if (migrated) cfg = migrated;
} else fixHints.push(`Run "${formatCliCommand("openclaw doctor --fix")}" to apply legacy migrations.`);
}
const normalized = normalizeLegacyConfigValues(candidate);
if (normalized.changes.length > 0) {
note$1(normalized.changes.join("\n"), "Doctor changes");
candidate = normalized.config;
pendingChanges = true;
if (shouldRepair) cfg = normalized.config;
else fixHints.push(`Run "${formatCliCommand("openclaw doctor --fix")}" to apply these changes.`);
}
const autoEnable = applyPluginAutoEnable({
config: candidate,
env: process.env
});
if (autoEnable.changes.length > 0) {
note$1(autoEnable.changes.join("\n"), "Doctor changes");
candidate = autoEnable.config;
pendingChanges = true;
if (shouldRepair) cfg = autoEnable.config;
else fixHints.push(`Run "${formatCliCommand("openclaw doctor --fix")}" to apply these changes.`);
}
const unknown = stripUnknownConfigKeys(candidate);
if (unknown.removed.length > 0) {
const lines = unknown.removed.map((path) => `- ${path}`).join("\n");
candidate = unknown.config;
pendingChanges = true;
if (shouldRepair) {
cfg = unknown.config;
note$1(lines, "Doctor changes");
} else {
note$1(lines, "Unknown config keys");
fixHints.push("Run \"openclaw doctor --fix\" to remove these keys.");
}
}
if (!shouldRepair && pendingChanges) {
if (await params.confirm({
message: "Apply recommended config repairs now?",
initialValue: true
})) {
cfg = candidate;
shouldWriteConfig = true;
} else if (fixHints.length > 0) note$1(fixHints.join("\n"), "Doctor");
}
noteOpencodeProviderOverrides(cfg);
return {
cfg,
path: snapshot.path ?? CONFIG_PATH,
shouldWriteConfig
};
}
//#endregion
//#region src/commands/doctor-format.ts
function formatGatewayRuntimeSummary(runtime) {
if (!runtime) return null;
const status = runtime.status ?? "unknown";
const details = [];
if (runtime.pid) details.push(`pid ${runtime.pid}`);
if (runtime.state && runtime.state.toLowerCase() !== status) details.push(`state ${runtime.state}`);
if (runtime.subState) details.push(`sub ${runtime.subState}`);
if (runtime.lastExitStatus !== void 0) details.push(`last exit ${runtime.lastExitStatus}`);
if (runtime.lastExitReason) details.push(`reason ${runtime.lastExitReason}`);
if (runtime.lastRunResult) details.push(`last run ${runtime.lastRunResult}`);
if (runtime.lastRunTime) details.push(`last run time ${runtime.lastRunTime}`);
if (runtime.detail) details.push(runtime.detail);
return details.length > 0 ? `${status} (${details.join(", ")})` : status;
}
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.f