UNPKG

@gguf/claw

Version:

WhatsApp gateway CLI (Baileys web) with Pi RPC agent

428 lines (425 loc) 15.4 kB
import { n as DEFAULT_GATEWAY_PORT, u as resolveGatewayPort } from "./paths-BDd7_JUB.js"; import { h as resolveUserPath } from "./utils-Dg0Xbl6w.js"; import { t as runCommandWithTimeout } from "./exec-CTo4hK94.js"; import { d as hasBinary } from "./skills-C4b1FA1e.js"; import path from "node:path"; import fs from "node:fs"; import { randomBytes } from "node:crypto"; //#region src/hooks/gmail.ts const DEFAULT_GMAIL_LABEL = "INBOX"; const DEFAULT_GMAIL_TOPIC = "gog-gmail-watch"; const DEFAULT_GMAIL_SUBSCRIPTION = "gog-gmail-watch-push"; const DEFAULT_GMAIL_SERVE_BIND = "127.0.0.1"; const DEFAULT_GMAIL_SERVE_PORT = 8788; const DEFAULT_GMAIL_SERVE_PATH = "/gmail-pubsub"; const DEFAULT_GMAIL_MAX_BYTES = 2e4; const DEFAULT_GMAIL_RENEW_MINUTES = 720; const DEFAULT_HOOKS_PATH = "/hooks"; function generateHookToken(bytes = 24) { return randomBytes(bytes).toString("hex"); } function mergeHookPresets(existing, preset) { const next = new Set((existing ?? []).map((item) => item.trim()).filter(Boolean)); next.add(preset); return Array.from(next); } function normalizeHooksPath(raw) { const base = raw?.trim() || DEFAULT_HOOKS_PATH; if (base === "/") return DEFAULT_HOOKS_PATH; return (base.startsWith("/") ? base : `/${base}`).replace(/\/+$/, ""); } function normalizeServePath(raw) { const base = raw?.trim() || DEFAULT_GMAIL_SERVE_PATH; if (base === "/") return "/"; return (base.startsWith("/") ? base : `/${base}`).replace(/\/+$/, ""); } function buildDefaultHookUrl(hooksPath, port = DEFAULT_GATEWAY_PORT) { const basePath = normalizeHooksPath(hooksPath); return joinUrl(`http://127.0.0.1:${port}`, `${basePath}/gmail`); } function resolveGmailHookRuntimeConfig(cfg, overrides) { const hooks = cfg.hooks; const gmail = hooks?.gmail; const hookToken = overrides.hookToken ?? hooks?.token ?? ""; if (!hookToken) return { ok: false, error: "hooks.token missing (needed for gmail hook)" }; const account = overrides.account ?? gmail?.account ?? ""; if (!account) return { ok: false, error: "gmail account required" }; const topic = overrides.topic ?? gmail?.topic ?? ""; if (!topic) return { ok: false, error: "gmail topic required" }; const subscription = overrides.subscription ?? gmail?.subscription ?? DEFAULT_GMAIL_SUBSCRIPTION; const pushToken = overrides.pushToken ?? gmail?.pushToken ?? ""; if (!pushToken) return { ok: false, error: "gmail push token required" }; const hookUrl = overrides.hookUrl ?? gmail?.hookUrl ?? buildDefaultHookUrl(hooks?.path, resolveGatewayPort(cfg)); const includeBody = overrides.includeBody ?? gmail?.includeBody ?? true; const maxBytesRaw = overrides.maxBytes ?? gmail?.maxBytes; const maxBytes = typeof maxBytesRaw === "number" && Number.isFinite(maxBytesRaw) && maxBytesRaw > 0 ? Math.floor(maxBytesRaw) : DEFAULT_GMAIL_MAX_BYTES; const renewEveryMinutesRaw = overrides.renewEveryMinutes ?? gmail?.renewEveryMinutes; const renewEveryMinutes = typeof renewEveryMinutesRaw === "number" && Number.isFinite(renewEveryMinutesRaw) && renewEveryMinutesRaw > 0 ? Math.floor(renewEveryMinutesRaw) : DEFAULT_GMAIL_RENEW_MINUTES; const serveBind = overrides.serveBind ?? gmail?.serve?.bind ?? DEFAULT_GMAIL_SERVE_BIND; const servePortRaw = overrides.servePort ?? gmail?.serve?.port; const servePort = typeof servePortRaw === "number" && Number.isFinite(servePortRaw) && servePortRaw > 0 ? Math.floor(servePortRaw) : DEFAULT_GMAIL_SERVE_PORT; const servePathRaw = overrides.servePath ?? gmail?.serve?.path; const normalizedServePathRaw = typeof servePathRaw === "string" && servePathRaw.trim().length > 0 ? normalizeServePath(servePathRaw) : DEFAULT_GMAIL_SERVE_PATH; const tailscaleTargetRaw = overrides.tailscaleTarget ?? gmail?.tailscale?.target; const tailscaleMode = overrides.tailscaleMode ?? gmail?.tailscale?.mode ?? "off"; const tailscaleTarget = tailscaleMode !== "off" && typeof tailscaleTargetRaw === "string" && tailscaleTargetRaw.trim().length > 0 ? tailscaleTargetRaw.trim() : void 0; const servePath = normalizeServePath(tailscaleMode !== "off" && !tailscaleTarget ? "/" : normalizedServePathRaw); const tailscalePathRaw = overrides.tailscalePath ?? gmail?.tailscale?.path; const tailscalePath = normalizeServePath(tailscaleMode !== "off" ? tailscalePathRaw ?? normalizedServePathRaw : tailscalePathRaw ?? servePath); return { ok: true, value: { account, label: overrides.label ?? gmail?.label ?? DEFAULT_GMAIL_LABEL, topic, subscription, pushToken, hookToken, hookUrl, includeBody, maxBytes, renewEveryMinutes, serve: { bind: serveBind, port: servePort, path: servePath }, tailscale: { mode: tailscaleMode, path: tailscalePath, target: tailscaleTarget } } }; } function buildGogWatchStartArgs(cfg) { return [ "gmail", "watch", "start", "--account", cfg.account, "--label", cfg.label, "--topic", cfg.topic ]; } function buildGogWatchServeArgs(cfg) { const args = [ "gmail", "watch", "serve", "--account", cfg.account, "--bind", cfg.serve.bind, "--port", String(cfg.serve.port), "--path", cfg.serve.path, "--token", cfg.pushToken, "--hook-url", cfg.hookUrl, "--hook-token", cfg.hookToken ]; if (cfg.includeBody) args.push("--include-body"); if (cfg.maxBytes > 0) args.push("--max-bytes", String(cfg.maxBytes)); return args; } function buildTopicPath(projectId, topicName) { return `projects/${projectId}/topics/${topicName}`; } function parseTopicPath(topic) { const match = topic.trim().match(/^projects\/([^/]+)\/topics\/([^/]+)$/i); if (!match) return null; return { projectId: match[1] ?? "", topicName: match[2] ?? "" }; } function joinUrl(base, path) { const url = new URL(base); url.pathname = `${url.pathname.replace(/\/+$/, "")}${path.startsWith("/") ? path : `/${path}`}`; return url.toString(); } //#endregion //#region src/hooks/gmail-setup-utils.ts let cachedPythonPath; const MAX_OUTPUT_CHARS = 800; function trimOutput(value) { const trimmed = value.trim(); if (!trimmed) return ""; if (trimmed.length <= MAX_OUTPUT_CHARS) return trimmed; return `${trimmed.slice(0, MAX_OUTPUT_CHARS)}…`; } function formatCommandFailure(command, result) { const code = result.code ?? "null"; const signal = result.signal ? `, signal=${result.signal}` : ""; const killed = result.killed ? ", killed=true" : ""; const stderr = trimOutput(result.stderr); const stdout = trimOutput(result.stdout); const lines = [`${command} failed (code=${code}${signal}${killed})`]; if (stderr) lines.push(`stderr: ${stderr}`); if (stdout) lines.push(`stdout: ${stdout}`); return lines.join("\n"); } function formatCommandResult(command, result) { const code = result.code ?? "null"; const signal = result.signal ? `, signal=${result.signal}` : ""; const killed = result.killed ? ", killed=true" : ""; const stderr = trimOutput(result.stderr); const stdout = trimOutput(result.stdout); const lines = [`${command} exited (code=${code}${signal}${killed})`]; if (stderr) lines.push(`stderr: ${stderr}`); if (stdout) lines.push(`stdout: ${stdout}`); return lines.join("\n"); } function formatJsonParseFailure(command, result, err) { return `${command} returned invalid JSON: ${err instanceof Error ? err.message : String(err)}\n${formatCommandResult(command, result)}`; } function formatCommand(command, args) { return [command, ...args].join(" "); } function findExecutablesOnPath(bins) { const parts = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean); const seen = /* @__PURE__ */ new Set(); const matches = []; for (const part of parts) for (const bin of bins) { const candidate = path.join(part, bin); if (seen.has(candidate)) continue; try { fs.accessSync(candidate, fs.constants.X_OK); matches.push(candidate); seen.add(candidate); } catch {} } return matches; } function ensurePathIncludes(dirPath, position) { const parts = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean); if (parts.includes(dirPath)) return; const next = position === "prepend" ? [dirPath, ...parts] : [...parts, dirPath]; process.env.PATH = next.join(path.delimiter); } function ensureGcloudOnPath() { if (hasBinary("gcloud")) return true; for (const candidate of [ "/opt/homebrew/share/google-cloud-sdk/bin/gcloud", "/usr/local/share/google-cloud-sdk/bin/gcloud", "/opt/homebrew/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/bin/gcloud", "/usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/bin/gcloud" ]) try { fs.accessSync(candidate, fs.constants.X_OK); ensurePathIncludes(path.dirname(candidate), "append"); return true; } catch {} return false; } async function resolvePythonExecutablePath() { if (cachedPythonPath !== void 0) return cachedPythonPath ?? void 0; const candidates = findExecutablesOnPath(["python3", "python"]); for (const candidate of candidates) { const res = await runCommandWithTimeout([ candidate, "-c", "import os, sys; print(os.path.realpath(sys.executable))" ], { timeoutMs: 2e3 }); if (res.code !== 0) continue; const resolved = res.stdout.trim().split(/\s+/)[0]; if (!resolved) continue; try { fs.accessSync(resolved, fs.constants.X_OK); cachedPythonPath = resolved; return resolved; } catch {} } cachedPythonPath = null; } async function gcloudEnv() { if (process.env.CLOUDSDK_PYTHON) return; const pythonPath = await resolvePythonExecutablePath(); if (!pythonPath) return; return { CLOUDSDK_PYTHON: pythonPath }; } async function runGcloudCommand(args, timeoutMs) { return await runCommandWithTimeout(["gcloud", ...args], { timeoutMs, env: await gcloudEnv() }); } async function ensureDependency(bin, brewArgs) { if (bin === "gcloud" && ensureGcloudOnPath()) return; if (hasBinary(bin)) return; if (process.platform !== "darwin") throw new Error(`${bin} not installed; install it and retry`); if (!hasBinary("brew")) throw new Error("Homebrew not installed (install brew and retry)"); const brewEnv = bin === "gcloud" ? await gcloudEnv() : void 0; const result = await runCommandWithTimeout([ "brew", "install", ...brewArgs ], { timeoutMs: 6e5, env: brewEnv }); if (result.code !== 0) throw new Error(`brew install failed for ${bin}: ${result.stderr || result.stdout}`); if (!hasBinary(bin)) throw new Error(`${bin} still not available after brew install`); } async function ensureGcloudAuth() { const res = await runGcloudCommand([ "auth", "list", "--filter", "status:ACTIVE", "--format", "value(account)" ], 3e4); if (res.code === 0 && res.stdout.trim()) return; const login = await runGcloudCommand(["auth", "login"], 6e5); if (login.code !== 0) throw new Error(login.stderr || "gcloud auth login failed"); } async function runGcloud(args) { const result = await runGcloudCommand(args, 12e4); if (result.code !== 0) throw new Error(result.stderr || result.stdout || "gcloud command failed"); return result; } async function ensureTopic(projectId, topicName) { if ((await runGcloudCommand([ "pubsub", "topics", "describe", topicName, "--project", projectId ], 3e4)).code === 0) return; await runGcloud([ "pubsub", "topics", "create", topicName, "--project", projectId ]); } async function ensureSubscription(projectId, subscription, topicName, pushEndpoint) { if ((await runGcloudCommand([ "pubsub", "subscriptions", "describe", subscription, "--project", projectId ], 3e4)).code === 0) { await runGcloud([ "pubsub", "subscriptions", "update", subscription, "--project", projectId, "--push-endpoint", pushEndpoint ]); return; } await runGcloud([ "pubsub", "subscriptions", "create", subscription, "--project", projectId, "--topic", topicName, "--push-endpoint", pushEndpoint ]); } async function ensureTailscaleEndpoint(params) { if (params.mode === "off") return ""; const statusArgs = ["status", "--json"]; const statusCommand = formatCommand("tailscale", statusArgs); const status = await runCommandWithTimeout(["tailscale", ...statusArgs], { timeoutMs: 3e4 }); if (status.code !== 0) throw new Error(formatCommandFailure(statusCommand, status)); let parsed; try { parsed = JSON.parse(status.stdout); } catch (err) { throw new Error(formatJsonParseFailure(statusCommand, status, err), { cause: err }); } const dnsName = parsed.Self?.DNSName?.replace(/\.$/, ""); if (!dnsName) throw new Error("tailscale DNS name missing; run tailscale up"); const target = typeof params.target === "string" && params.target.trim().length > 0 ? params.target.trim() : params.port ? String(params.port) : ""; if (!target) throw new Error("tailscale target missing; set a port or target URL"); const pathArg = normalizeServePath(params.path); const funnelArgs = [ params.mode, "--bg", "--set-path", pathArg, "--yes", target ]; const funnelCommand = formatCommand("tailscale", funnelArgs); const funnelResult = await runCommandWithTimeout(["tailscale", ...funnelArgs], { timeoutMs: 3e4 }); if (funnelResult.code !== 0) throw new Error(formatCommandFailure(funnelCommand, funnelResult)); const baseUrl = `https://${dnsName}${pathArg}`; return params.token ? `${baseUrl}?token=${params.token}` : baseUrl; } async function resolveProjectIdFromGogCredentials() { const candidates = gogCredentialsPaths(); for (const candidate of candidates) { if (!fs.existsSync(candidate)) continue; try { const raw = fs.readFileSync(candidate, "utf-8"); const projectNumber = extractProjectNumber(extractGogClientId(JSON.parse(raw))); if (!projectNumber) continue; const res = await runGcloudCommand([ "projects", "list", "--filter", `projectNumber=${projectNumber}`, "--format", "value(projectId)" ], 3e4); if (res.code !== 0) continue; const projectId = res.stdout.trim().split(/\s+/)[0]; if (projectId) return projectId; } catch {} } return null; } function gogCredentialsPaths() { const paths = []; const xdg = process.env.XDG_CONFIG_HOME; if (xdg) paths.push(path.join(xdg, "gogcli", "credentials.json")); paths.push(resolveUserPath("~/.config/gogcli/credentials.json")); if (process.platform === "darwin") paths.push(resolveUserPath("~/Library/Application Support/gogcli/credentials.json")); return paths; } function extractGogClientId(parsed) { const installed = parsed.installed; const web = parsed.web; const candidate = installed?.client_id || web?.client_id || parsed.client_id || ""; return typeof candidate === "string" ? candidate : null; } function extractProjectNumber(clientId) { if (!clientId) return null; return clientId.match(/^(\d+)-/)?.[1] ?? null; } //#endregion export { normalizeServePath as C, normalizeHooksPath as S, resolveGmailHookRuntimeConfig as T, buildGogWatchServeArgs as _, ensureTopic as a, generateHookToken as b, DEFAULT_GMAIL_LABEL as c, DEFAULT_GMAIL_SERVE_BIND as d, DEFAULT_GMAIL_SERVE_PATH as f, buildDefaultHookUrl as g, DEFAULT_GMAIL_TOPIC as h, ensureTailscaleEndpoint as i, DEFAULT_GMAIL_MAX_BYTES as l, DEFAULT_GMAIL_SUBSCRIPTION as m, ensureGcloudAuth as n, resolveProjectIdFromGogCredentials as o, DEFAULT_GMAIL_SERVE_PORT as p, ensureSubscription as r, runGcloud as s, ensureDependency as t, DEFAULT_GMAIL_RENEW_MINUTES as u, buildGogWatchStartArgs as v, parseTopicPath as w, mergeHookPresets as x, buildTopicPath as y };