UNPKG

@gguf/claw

Version:

WhatsApp gateway CLI (Baileys web) with Pi RPC agent

542 lines (538 loc) 18.7 kB
import { d as resolveGatewaySystemdServiceName, f as resolveGatewayWindowsTaskName, l as resolveGatewayLaunchAgentLabel, n as GATEWAY_SERVICE_KIND, r as GATEWAY_SERVICE_MARKER } from "./constants-3er_81qc.js"; import { o as resolveGatewayLogPaths, s as resolveLaunchAgentPlistPath } from "./service-CAxAjHNB.js"; import { o as resolveSystemdUserUnitPath } from "./systemd-CNYEkRek.js"; import { c as getMinimalServicePathPartsFromEnv, l as isSystemNodePath, m as resolveSystemNodePath, u as isVersionManagedNodePath } from "./daemon-runtime-vNkYv9tq.js"; import path from "node:path"; import fs from "node:fs/promises"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; //#region src/daemon/diagnostics.ts const GATEWAY_LOG_ERROR_PATTERNS = [ /refusing to bind gateway/i, /gateway auth mode/i, /gateway start blocked/i, /failed to bind gateway socket/i, /tailscale .* requires/i ]; async function readLastLogLine(filePath) { try { const lines = (await fs.readFile(filePath, "utf8")).split(/\r?\n/).map((line) => line.trim()); for (let i = lines.length - 1; i >= 0; i -= 1) if (lines[i]) return lines[i]; return null; } catch { return null; } } async function readLastGatewayErrorLine(env) { const { stdoutPath, stderrPath } = resolveGatewayLogPaths(env); const stderrRaw = await fs.readFile(stderrPath, "utf8").catch(() => ""); const stdoutRaw = await fs.readFile(stdoutPath, "utf8").catch(() => ""); const lines = [...stderrRaw.split(/\r?\n/), ...stdoutRaw.split(/\r?\n/)].map((line) => line.trim()); for (let i = lines.length - 1; i >= 0; i -= 1) { const line = lines[i]; if (!line) continue; if (GATEWAY_LOG_ERROR_PATTERNS.some((pattern) => pattern.test(line))) return line; } return await readLastLogLine(stderrPath) ?? await readLastLogLine(stdoutPath); } //#endregion //#region src/daemon/inspect.ts const EXTRA_MARKERS = [ "openclaw", "clawdbot", "moltbot" ]; const execFileAsync = promisify(execFile); function renderGatewayServiceCleanupHints(env = process.env) { const profile = env.OPENCLAW_PROFILE; switch (process.platform) { case "darwin": { const label = resolveGatewayLaunchAgentLabel(profile); return [`launchctl bootout gui/$UID/${label}`, `rm ~/Library/LaunchAgents/${label}.plist`]; } case "linux": { const unit = resolveGatewaySystemdServiceName(profile); return [`systemctl --user disable --now ${unit}.service`, `rm ~/.config/systemd/user/${unit}.service`]; } case "win32": return [`schtasks /Delete /TN "${resolveGatewayWindowsTaskName(profile)}" /F`]; default: return []; } } function resolveHomeDir(env) { const home = env.HOME?.trim() || env.USERPROFILE?.trim(); if (!home) throw new Error("Missing HOME"); return home; } function detectMarker(content) { const lower = content.toLowerCase(); for (const marker of EXTRA_MARKERS) if (lower.includes(marker)) return marker; return null; } function hasGatewayServiceMarker(content) { const lower = content.toLowerCase(); const markerKeys = ["openclaw_service_marker"]; const kindKeys = ["openclaw_service_kind"]; const markerValues = [GATEWAY_SERVICE_MARKER.toLowerCase()]; const hasMarkerKey = markerKeys.some((key) => lower.includes(key)); const hasKindKey = kindKeys.some((key) => lower.includes(key)); const hasMarkerValue = markerValues.some((value) => lower.includes(value)); return hasMarkerKey && hasKindKey && hasMarkerValue && lower.includes(GATEWAY_SERVICE_KIND.toLowerCase()); } function isOpenClawGatewayLaunchdService(label, contents) { if (hasGatewayServiceMarker(contents)) return true; if (!contents.toLowerCase().includes("gateway")) return false; return label.startsWith("ai.openclaw."); } function isOpenClawGatewaySystemdService(name, contents) { if (hasGatewayServiceMarker(contents)) return true; if (!name.startsWith("openclaw-gateway")) return false; return contents.toLowerCase().includes("gateway"); } function isOpenClawGatewayTaskName(name) { const normalized = name.trim().toLowerCase(); if (!normalized) return false; return normalized === resolveGatewayWindowsTaskName().toLowerCase() || normalized.startsWith("openclaw gateway"); } function tryExtractPlistLabel(contents) { const match = contents.match(/<key>Label<\/key>\s*<string>([\s\S]*?)<\/string>/i); if (!match) return null; return match[1]?.trim() || null; } function isIgnoredLaunchdLabel(label) { return label === resolveGatewayLaunchAgentLabel(); } function isIgnoredSystemdName(name) { return name === resolveGatewaySystemdServiceName(); } function isLegacyLabel(label) { const lower = label.toLowerCase(); return lower.includes("clawdbot") || lower.includes("moltbot"); } async function scanLaunchdDir(params) { const results = []; let entries = []; try { entries = await fs.readdir(params.dir); } catch { return results; } for (const entry of entries) { if (!entry.endsWith(".plist")) continue; const labelFromName = entry.replace(/\.plist$/, ""); if (isIgnoredLaunchdLabel(labelFromName)) continue; const fullPath = path.join(params.dir, entry); let contents = ""; try { contents = await fs.readFile(fullPath, "utf8"); } catch { continue; } const marker = detectMarker(contents); const label = tryExtractPlistLabel(contents) ?? labelFromName; if (!marker) { if (!(isLegacyLabel(labelFromName) || isLegacyLabel(label))) continue; results.push({ platform: "darwin", label, detail: `plist: ${fullPath}`, scope: params.scope, marker: isLegacyLabel(label) ? "clawdbot" : "moltbot", legacy: true }); continue; } if (isIgnoredLaunchdLabel(label)) continue; if (marker === "openclaw" && isOpenClawGatewayLaunchdService(label, contents)) continue; results.push({ platform: "darwin", label, detail: `plist: ${fullPath}`, scope: params.scope, marker, legacy: marker !== "openclaw" || isLegacyLabel(label) }); } return results; } async function scanSystemdDir(params) { const results = []; let entries = []; try { entries = await fs.readdir(params.dir); } catch { return results; } for (const entry of entries) { if (!entry.endsWith(".service")) continue; const name = entry.replace(/\.service$/, ""); if (isIgnoredSystemdName(name)) continue; const fullPath = path.join(params.dir, entry); let contents = ""; try { contents = await fs.readFile(fullPath, "utf8"); } catch { continue; } const marker = detectMarker(contents); if (!marker) continue; if (marker === "openclaw" && isOpenClawGatewaySystemdService(name, contents)) continue; results.push({ platform: "linux", label: entry, detail: `unit: ${fullPath}`, scope: params.scope, marker, legacy: marker !== "openclaw" }); } return results; } function parseSchtasksList(output) { const tasks = []; let current = null; for (const rawLine of output.split(/\r?\n/)) { const line = rawLine.trim(); if (!line) { if (current) { tasks.push(current); current = null; } continue; } const idx = line.indexOf(":"); if (idx <= 0) continue; const key = line.slice(0, idx).trim().toLowerCase(); const value = line.slice(idx + 1).trim(); if (!value) continue; if (key === "taskname") { if (current) tasks.push(current); current = { name: value }; continue; } if (!current) continue; if (key === "task to run") current.taskToRun = value; } if (current) tasks.push(current); return tasks; } async function execSchtasks(args) { try { const { stdout, stderr } = await execFileAsync("schtasks", args, { encoding: "utf8", windowsHide: true }); return { stdout: String(stdout ?? ""), stderr: String(stderr ?? ""), code: 0 }; } catch (error) { const e = error; return { stdout: typeof e.stdout === "string" ? e.stdout : "", stderr: typeof e.stderr === "string" ? e.stderr : typeof e.message === "string" ? e.message : "", code: typeof e.code === "number" ? e.code : 1 }; } } async function findExtraGatewayServices(env, opts = {}) { const results = []; const seen = /* @__PURE__ */ new Set(); const push = (svc) => { const key = `${svc.platform}:${svc.label}:${svc.detail}:${svc.scope}`; if (seen.has(key)) return; seen.add(key); results.push(svc); }; if (process.platform === "darwin") { try { const home = resolveHomeDir(env); const userDir = path.join(home, "Library", "LaunchAgents"); for (const svc of await scanLaunchdDir({ dir: userDir, scope: "user" })) push(svc); if (opts.deep) { for (const svc of await scanLaunchdDir({ dir: path.join(path.sep, "Library", "LaunchAgents"), scope: "system" })) push(svc); for (const svc of await scanLaunchdDir({ dir: path.join(path.sep, "Library", "LaunchDaemons"), scope: "system" })) push(svc); } } catch { return results; } return results; } if (process.platform === "linux") { try { const home = resolveHomeDir(env); const userDir = path.join(home, ".config", "systemd", "user"); for (const svc of await scanSystemdDir({ dir: userDir, scope: "user" })) push(svc); if (opts.deep) for (const dir of [ "/etc/systemd/system", "/usr/lib/systemd/system", "/lib/systemd/system" ]) for (const svc of await scanSystemdDir({ dir, scope: "system" })) push(svc); } catch { return results; } return results; } if (process.platform === "win32") { if (!opts.deep) return results; const res = await execSchtasks([ "/Query", "/FO", "LIST", "/V" ]); if (res.code !== 0) return results; const tasks = parseSchtasksList(res.stdout); for (const task of tasks) { const name = task.name.trim(); if (!name) continue; if (isOpenClawGatewayTaskName(name)) continue; const lowerName = name.toLowerCase(); const lowerCommand = task.taskToRun?.toLowerCase() ?? ""; let marker = null; for (const candidate of EXTRA_MARKERS) if (lowerName.includes(candidate) || lowerCommand.includes(candidate)) { marker = candidate; break; } if (!marker) continue; push({ platform: "win32", label: name, detail: task.taskToRun ? `task: ${name}, run: ${task.taskToRun}` : name, scope: "system", marker, legacy: marker !== "openclaw" }); } return results; } return results; } //#endregion //#region src/daemon/service-audit.ts const SERVICE_AUDIT_CODES = { gatewayCommandMissing: "gateway-command-missing", gatewayEntrypointMismatch: "gateway-entrypoint-mismatch", gatewayPathMissing: "gateway-path-missing", gatewayPathMissingDirs: "gateway-path-missing-dirs", gatewayPathNonMinimal: "gateway-path-nonminimal", gatewayRuntimeBun: "gateway-runtime-bun", gatewayRuntimeNodeVersionManager: "gateway-runtime-node-version-manager", gatewayRuntimeNodeSystemMissing: "gateway-runtime-node-system-missing", launchdKeepAlive: "launchd-keep-alive", launchdRunAtLoad: "launchd-run-at-load", systemdAfterNetworkOnline: "systemd-after-network-online", systemdRestartSec: "systemd-restart-sec", systemdWantsNetworkOnline: "systemd-wants-network-online" }; function needsNodeRuntimeMigration(issues) { return issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayRuntimeBun || issue.code === SERVICE_AUDIT_CODES.gatewayRuntimeNodeVersionManager); } function hasGatewaySubcommand(programArguments) { return Boolean(programArguments?.some((arg) => arg === "gateway")); } function parseSystemdUnit(content) { const after = /* @__PURE__ */ new Set(); const wants = /* @__PURE__ */ new Set(); let restartSec; for (const rawLine of content.split(/\r?\n/)) { const line = rawLine.trim(); if (!line) continue; if (line.startsWith("#") || line.startsWith(";")) continue; if (line.startsWith("[")) continue; const idx = line.indexOf("="); if (idx <= 0) continue; const key = line.slice(0, idx).trim(); const value = line.slice(idx + 1).trim(); if (!value) continue; if (key === "After") { for (const entry of value.split(/\s+/)) if (entry) after.add(entry); } else if (key === "Wants") { for (const entry of value.split(/\s+/)) if (entry) wants.add(entry); } else if (key === "RestartSec") restartSec = value; } return { after, wants, restartSec }; } function isRestartSecPreferred(value) { if (!value) return false; const parsed = Number.parseFloat(value); if (!Number.isFinite(parsed)) return false; return Math.abs(parsed - 5) < .01; } async function auditSystemdUnit(env, issues) { const unitPath = resolveSystemdUserUnitPath(env); let content = ""; try { content = await fs.readFile(unitPath, "utf8"); } catch { return; } const parsed = parseSystemdUnit(content); if (!parsed.after.has("network-online.target")) issues.push({ code: SERVICE_AUDIT_CODES.systemdAfterNetworkOnline, message: "Missing systemd After=network-online.target", detail: unitPath, level: "recommended" }); if (!parsed.wants.has("network-online.target")) issues.push({ code: SERVICE_AUDIT_CODES.systemdWantsNetworkOnline, message: "Missing systemd Wants=network-online.target", detail: unitPath, level: "recommended" }); if (!isRestartSecPreferred(parsed.restartSec)) issues.push({ code: SERVICE_AUDIT_CODES.systemdRestartSec, message: "RestartSec does not match the recommended 5s", detail: unitPath, level: "recommended" }); } async function auditLaunchdPlist(env, issues) { const plistPath = resolveLaunchAgentPlistPath(env); let content = ""; try { content = await fs.readFile(plistPath, "utf8"); } catch { return; } const hasRunAtLoad = /<key>RunAtLoad<\/key>\s*<true\s*\/>/i.test(content); const hasKeepAlive = /<key>KeepAlive<\/key>\s*<true\s*\/>/i.test(content); if (!hasRunAtLoad) issues.push({ code: SERVICE_AUDIT_CODES.launchdRunAtLoad, message: "LaunchAgent is missing RunAtLoad=true", detail: plistPath, level: "recommended" }); if (!hasKeepAlive) issues.push({ code: SERVICE_AUDIT_CODES.launchdKeepAlive, message: "LaunchAgent is missing KeepAlive=true", detail: plistPath, level: "recommended" }); } function auditGatewayCommand(programArguments, issues) { if (!programArguments || programArguments.length === 0) return; if (!hasGatewaySubcommand(programArguments)) issues.push({ code: SERVICE_AUDIT_CODES.gatewayCommandMissing, message: "Service command does not include the gateway subcommand", level: "aggressive" }); } function isNodeRuntime(execPath) { const base = path.basename(execPath).toLowerCase(); return base === "node" || base === "node.exe"; } function isBunRuntime(execPath) { const base = path.basename(execPath).toLowerCase(); return base === "bun" || base === "bun.exe"; } function getPathModule(platform) { return platform === "win32" ? path.win32 : path.posix; } function normalizePathEntry(entry, platform) { const normalized = getPathModule(platform).normalize(entry).replaceAll("\\", "/"); if (platform === "win32") return normalized.toLowerCase(); return normalized; } function auditGatewayServicePath(command, issues, env, platform) { if (platform === "win32") return; const servicePath = command?.environment?.PATH; if (!servicePath) { issues.push({ code: SERVICE_AUDIT_CODES.gatewayPathMissing, message: "Gateway service PATH is not set; the daemon should use a minimal PATH.", level: "recommended" }); return; } const expected = getMinimalServicePathPartsFromEnv({ platform, env }); const parts = servicePath.split(getPathModule(platform).delimiter).map((entry) => entry.trim()).filter(Boolean); const normalizedParts = new Set(parts.map((entry) => normalizePathEntry(entry, platform))); const normalizedExpected = new Set(expected.map((entry) => normalizePathEntry(entry, platform))); const missing = expected.filter((entry) => { const normalized = normalizePathEntry(entry, platform); return !normalizedParts.has(normalized); }); if (missing.length > 0) issues.push({ code: SERVICE_AUDIT_CODES.gatewayPathMissingDirs, message: `Gateway service PATH missing required dirs: ${missing.join(", ")}`, level: "recommended" }); const nonMinimal = parts.filter((entry) => { const normalized = normalizePathEntry(entry, platform); if (normalizedExpected.has(normalized)) return false; return normalized.includes("/.nvm/") || normalized.includes("/.fnm/") || normalized.includes("/.volta/") || normalized.includes("/.asdf/") || normalized.includes("/.n/") || normalized.includes("/.nodenv/") || normalized.includes("/.nodebrew/") || normalized.includes("/nvs/") || normalized.includes("/.local/share/pnpm/") || normalized.includes("/pnpm/") || normalized.endsWith("/pnpm"); }); if (nonMinimal.length > 0) issues.push({ code: SERVICE_AUDIT_CODES.gatewayPathNonMinimal, message: "Gateway service PATH includes version managers or package managers; recommend a minimal PATH.", detail: nonMinimal.join(", "), level: "recommended" }); } async function auditGatewayRuntime(env, command, issues, platform) { const execPath = command?.programArguments?.[0]; if (!execPath) return; if (isBunRuntime(execPath)) { issues.push({ code: SERVICE_AUDIT_CODES.gatewayRuntimeBun, message: "Gateway service uses Bun; Bun is incompatible with WhatsApp + Telegram channels.", detail: execPath, level: "recommended" }); return; } if (!isNodeRuntime(execPath)) return; if (isVersionManagedNodePath(execPath, platform)) { issues.push({ code: SERVICE_AUDIT_CODES.gatewayRuntimeNodeVersionManager, message: "Gateway service uses Node from a version manager; it can break after upgrades.", detail: execPath, level: "recommended" }); if (!isSystemNodePath(execPath, env, platform)) { if (!await resolveSystemNodePath(env, platform)) issues.push({ code: SERVICE_AUDIT_CODES.gatewayRuntimeNodeSystemMissing, message: "System Node 22+ not found; install it before migrating away from version managers.", level: "recommended" }); } } } async function auditGatewayServiceConfig(params) { const issues = []; const platform = params.platform ?? process.platform; auditGatewayCommand(params.command?.programArguments, issues); auditGatewayServicePath(params.command, issues, params.env, platform); await auditGatewayRuntime(params.env, params.command, issues, platform); if (platform === "linux") await auditSystemdUnit(params.env, issues); else if (platform === "darwin") await auditLaunchdPlist(params.env, issues); return { ok: issues.length === 0, issues }; } //#endregion export { renderGatewayServiceCleanupHints as a, findExtraGatewayServices as i, auditGatewayServiceConfig as n, readLastGatewayErrorLine as o, needsNodeRuntimeMigration as r, SERVICE_AUDIT_CODES as t };