Download src/hooks/gmail-setup-utils.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 11.3 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/hooks/gmail-setup-utils.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/hooks/gmail-setup-utils.ts
-
curl -L -o gmail-setup-utils.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/hooks/gmail-setup-utils.ts
11.3 kB
| // Gmail setup utilities write helper files and normalize Gmail setup settings. | |
| import fs from "node:fs"; | |
| import path from "node:path"; | |
| import { formatErrorMessage } from "../infra/errors.js"; | |
| import { resolveExecutable } from "../infra/executable-path.js"; | |
| import { formatCommandOutput, formatCommandResult } from "../process/command-error.js"; | |
| import { runCommandWithTimeout, type SpawnResult } from "../process/exec.js"; | |
| import { hasBinary } from "../skills/loading/config.js"; | |
| import { resolveUserPath } from "../utils.js"; | |
| import { normalizeServePath } from "./gmail.js"; | |
| let cachedPythonPath: string | null | undefined; | |
| let gcloudBin: string | undefined; | |
| function formatJsonParseFailure(command: string, result: SpawnResult, err: unknown): string { | |
| const reason = formatCommandOutput(formatErrorMessage(err)); | |
| return `${command} returned invalid JSON: ${reason}\n${formatCommandResult(command, result)}`; | |
| } | |
| function findExecutablesOnPath(bins: string[]): string[] { | |
| const pathEnv = process.env.PATH ?? ""; | |
| const parts = pathEnv.split(path.delimiter).filter(Boolean); | |
| const seen = new Set<string>(); | |
| const matches: string[] = []; | |
| 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 { | |
| // keep scanning | |
| } | |
| } | |
| } | |
| return matches; | |
| } | |
| function ensurePathIncludes(dirPath: string, position: "append" | "prepend") { | |
| const pathEnv = process.env.PATH ?? ""; | |
| const parts = pathEnv.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(): boolean { | |
| if (hasBinary("gcloud")) { | |
| return true; | |
| } | |
| const candidates = [ | |
| "/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", | |
| ]; | |
| for (const candidate of candidates) { | |
| try { | |
| fs.accessSync(candidate, fs.constants.X_OK); | |
| ensurePathIncludes(path.dirname(candidate), "append"); | |
| return true; | |
| } catch { | |
| // keep scanning | |
| } | |
| } | |
| return false; | |
| } | |
| // gcloud requires a Python interpreter in this range to run; picking an | |
| // interpreter outside it makes `gcloud` fail to load. See `gcloud topic startup`. | |
| const MIN_GCLOUD_PYTHON: readonly [number, number] = [3, 10]; | |
| const MAX_GCLOUD_PYTHON: readonly [number, number] = [3, 14]; | |
| function isSupportedGcloudPythonVersion(major: number, minor: number): boolean { | |
| if (major !== MIN_GCLOUD_PYTHON[0]) { | |
| return false; | |
| } | |
| return minor >= MIN_GCLOUD_PYTHON[1] && minor <= MAX_GCLOUD_PYTHON[1]; | |
| } | |
| async function resolvePythonExecutablePath(): Promise<string | undefined> { | |
| if (cachedPythonPath !== undefined) { | |
| return cachedPythonPath ?? undefined; | |
| } | |
| 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)); print('%d.%d' % sys.version_info[:2])", | |
| ], | |
| { timeoutMs: 2_000 }, | |
| ); | |
| if (res.code !== 0) { | |
| continue; | |
| } | |
| const lines = res.stdout.trim().split(/\r?\n/); | |
| const resolved = lines[0]?.trim().split(/\s+/)[0]; | |
| if (!resolved) { | |
| continue; | |
| } | |
| const version = lines[1]?.trim().match(/^(\d+)\.(\d+)/); | |
| if (!version) { | |
| continue; | |
| } | |
| if (!isSupportedGcloudPythonVersion(Number(version[1]), Number(version[2]))) { | |
| // Skip interpreters gcloud cannot use (e.g. macOS' bundled Python 3.9) | |
| // so a compatible interpreter later on PATH is selected instead. | |
| continue; | |
| } | |
| try { | |
| fs.accessSync(resolved, fs.constants.X_OK); | |
| cachedPythonPath = resolved; | |
| return resolved; | |
| } catch { | |
| // keep scanning | |
| } | |
| } | |
| cachedPythonPath = null; | |
| return undefined; | |
| } | |
| async function gcloudEnv(): Promise<NodeJS.ProcessEnv> { | |
| const pythonPath = await resolvePythonExecutablePath(); | |
| // Always override inherited gcloud Python controls so the launcher cannot | |
| // select a workspace-controlled interpreter or word-split injected args. | |
| return { CLOUDSDK_PYTHON: pythonPath, CLOUDSDK_PYTHON_ARGS: undefined }; | |
| } | |
| async function runGcloudCommand( | |
| args: string[], | |
| timeoutMs: number, | |
| ): Promise<Awaited<ReturnType<typeof runCommandWithTimeout>>> { | |
| return await runCommandWithTimeout([(gcloudBin ??= resolveExecutable("gcloud")), ...args], { | |
| timeoutMs, | |
| env: await gcloudEnv(), | |
| }); | |
| } | |
| export async function ensureDependency(bin: string, brewArgs: string[]) { | |
| 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() : undefined; | |
| const result = await runCommandWithTimeout(["brew", "install", ...brewArgs], { | |
| timeoutMs: 600_000, | |
| env: brewEnv, | |
| }); | |
| if (result.code !== 0) { | |
| throw new Error(formatCommandResult(`brew install for ${bin}`, result)); | |
| } | |
| if (!hasBinary(bin)) { | |
| throw new Error(`${bin} still not available after brew install`); | |
| } | |
| } | |
| export async function ensureGcloudAuth() { | |
| const res = await runGcloudCommand( | |
| ["auth", "list", "--filter", "status:ACTIVE", "--format", "value(account)"], | |
| 30_000, | |
| ); | |
| if (res.code === 0 && res.stdout.trim()) { | |
| return; | |
| } | |
| const login = await runGcloudCommand(["auth", "login"], 600_000); | |
| if (login.code !== 0) { | |
| throw new Error(formatCommandResult("gcloud auth login", login)); | |
| } | |
| } | |
| export async function runGcloud(args: string[]) { | |
| const result = await runGcloudCommand(args, 120_000); | |
| if (result.code !== 0) { | |
| throw new Error(formatCommandResult("gcloud command", result)); | |
| } | |
| return result; | |
| } | |
| export async function ensureTopic(projectId: string, topicName: string) { | |
| const describe = await runGcloudCommand( | |
| ["pubsub", "topics", "describe", topicName, "--project", projectId], | |
| 30_000, | |
| ); | |
| if (describe.code === 0) { | |
| return; | |
| } | |
| await runGcloud(["pubsub", "topics", "create", topicName, "--project", projectId]); | |
| } | |
| export async function ensureSubscription( | |
| projectId: string, | |
| subscription: string, | |
| topicName: string, | |
| pushEndpoint: string, | |
| ) { | |
| const describe = await runGcloudCommand( | |
| ["pubsub", "subscriptions", "describe", subscription, "--project", projectId], | |
| 30_000, | |
| ); | |
| if (describe.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, | |
| ]); | |
| } | |
| export async function ensureTailscaleEndpoint(params: { | |
| mode: "off" | "serve" | "funnel"; | |
| path: string; | |
| port?: number; | |
| signal?: AbortSignal; | |
| target?: string; | |
| token?: string; | |
| }): Promise<string> { | |
| if (params.mode === "off") { | |
| return ""; | |
| } | |
| const tailscaleBin = resolveExecutable("tailscale"); | |
| const statusArgs = ["status", "--json"]; | |
| const statusCommand = "tailscale status --json"; | |
| const status = await runCommandWithTimeout([tailscaleBin, ...statusArgs], { | |
| timeoutMs: 30_000, | |
| signal: params.signal, | |
| }); | |
| if (status.code !== 0) { | |
| throw new Error(formatCommandResult(statusCommand, status)); | |
| } | |
| let parsed: { Self?: { DNSName?: string } }; | |
| try { | |
| parsed = JSON.parse(status.stdout) as { Self?: { DNSName?: string } }; | |
| } 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 funnelResult = await runCommandWithTimeout([tailscaleBin, ...funnelArgs], { | |
| timeoutMs: 30_000, | |
| signal: params.signal, | |
| }); | |
| if (funnelResult.code !== 0) { | |
| throw new Error(formatCommandResult(`tailscale ${params.mode}`, funnelResult)); | |
| } | |
| const baseUrl = `https://${dnsName}${pathArg}`; | |
| // Funnel/serve strips pathArg before proxying; keep it only in the public URL. | |
| return params.token ? `${baseUrl}?token=${params.token}` : baseUrl; | |
| } | |
| export async function resolveProjectIdFromGogCredentials(): Promise<string | null> { | |
| const candidates = gogCredentialsPaths(); | |
| for (const candidate of candidates) { | |
| if (!fs.existsSync(candidate)) { | |
| continue; | |
| } | |
| try { | |
| const raw = fs.readFileSync(candidate, "utf-8"); | |
| const parsed = JSON.parse(raw) as Record<string, unknown>; | |
| const clientId = extractGogClientId(parsed); | |
| const projectNumber = extractProjectNumber(clientId); | |
| if (!projectNumber) { | |
| continue; | |
| } | |
| const res = await runGcloudCommand( | |
| [ | |
| "projects", | |
| "list", | |
| "--filter", | |
| `projectNumber=${projectNumber}`, | |
| "--format", | |
| "value(projectId)", | |
| ], | |
| 30_000, | |
| ); | |
| if (res.code !== 0) { | |
| continue; | |
| } | |
| const projectId = res.stdout.trim().split(/\s+/)[0]; | |
| if (projectId) { | |
| return projectId; | |
| } | |
| } catch { | |
| // keep scanning | |
| } | |
| } | |
| return null; | |
| } | |
| function gogCredentialsPaths(): string[] { | |
| const paths: string[] = []; | |
| 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: Record<string, unknown>): string | null { | |
| const installed = parsed.installed as Record<string, unknown> | undefined; | |
| const web = parsed.web as Record<string, unknown> | undefined; | |
| const candidate = installed?.client_id || web?.client_id || parsed.client_id || ""; | |
| return typeof candidate === "string" ? candidate : null; | |
| } | |
| function extractProjectNumber(clientId: string | null): string | null { | |
| if (!clientId) { | |
| return null; | |
| } | |
| const match = clientId.match(/^(\d+)-/); | |
| return match?.[1] ?? null; | |
| } | |