import type { Command } from "commander" ;
import { danger } from "../globals.js" ;
import {
type GmailRunOptions,
type GmailSetupOptions,
runGmailService,
runGmailSetup,
} from "../hooks/gmail-ops.js" ;
import {
DEFAULT_GMAIL_LABEL,
DEFAULT_GMAIL_MAX_BYTES,
DEFAULT_GMAIL_RENEW_MINUTES,
DEFAULT_GMAIL_SERVE_BIND,
DEFAULT_GMAIL_SERVE_PATH,
DEFAULT_GMAIL_SERVE_PORT,
DEFAULT_GMAIL_SUBSCRIPTION,
DEFAULT_GMAIL_TOPIC,
} from "../hooks/gmail.js" ;
import { defaultRuntime } from "../runtime.js" ;
import { normalizeOptionalString } from "../shared/string-coerce.js" ;
import { formatDocsLink } from "../terminal/links.js" ;
import { theme } from "../terminal/theme.js" ;
export function registerWebhooksCli(program: Command) {
const webhooks = program
.command("webhooks" )
.description("Webhook helpers and integrations" )
.addHelpText(
"after" ,
() =>
`\n${theme.muted("Docs:" )} ${formatDocsLink("/cli/webhooks" , "docs.openclaw.ai/cli/webhooks" )}\n`,
);
const gmail = webhooks.command("gmail" ).description("Gmail Pub/Sub hooks (via gogcli)" );
gmail
.command("setup" )
.description("Configure Gmail watch + Pub/Sub + OpenClaw hooks" )
.requiredOption("--account <email>" , "Gmail account to watch" )
.option("--project <id>" , "GCP project id (OAuth client owner)" )
.option("--topic <name>" , "Pub/Sub topic name" , DEFAULT_GMAIL_TOPIC)
.option("--subscription <name>" , "Pub/Sub subscription name" , DEFAULT_GMAIL_SUBSCRIPTION)
.option("--label <label>" , "Gmail label to watch" , DEFAULT_GMAIL_LABEL)
.option("--hook-url <url>" , "OpenClaw hook URL" )
.option("--hook-token <token>" , "OpenClaw hook token" )
.option("--push-token <token>" , "Push token for gog watch serve" )
.option("--bind <host>" , "gog watch serve bind host" , DEFAULT_GMAIL_SERVE_BIND)
.option("--port <port>" , "gog watch serve port" , String(DEFAULT_GMAIL_SERVE_PORT))
.option("--path <path>" , "gog watch serve path" , DEFAULT_GMAIL_SERVE_PATH)
.option("--include-body" , "Include email body snippets" , true )
.option("--max-bytes <n>" , "Max bytes for body snippets" , String(DEFAULT_GMAIL_MAX_BYTES))
.option(
"--renew-minutes <n>" ,
"Renew watch every N minutes" ,
String(DEFAULT_GMAIL_RENEW_MINUTES),
)
.option("--tailscale <mode>" , "Expose push endpoint via tailscale (funnel|serve|off)" , "funnel" )
.option("--tailscale-path <path>" , "Path for tailscale serve/funnel" )
.option(
"--tailscale-target <target>" ,
"Tailscale serve/funnel target (port, host:port, or URL)" ,
)
.option("--push-endpoint <url>" , "Explicit Pub/Sub push endpoint" )
.option("--json" , "Output JSON summary" , false )
.action(async (opts) => {
try {
const parsed = parseGmailSetupOptions(opts);
await runGmailSetup(parsed);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1 );
}
});
gmail
.command("run" )
.description("Run gog watch serve + auto-renew loop" )
.option("--account <email>" , "Gmail account to watch" )
.option("--topic <topic>" , "Pub/Sub topic path (projects/.../topics/..)" )
.option("--subscription <name>" , "Pub/Sub subscription name" )
.option("--label <label>" , "Gmail label to watch" )
.option("--hook-url <url>" , "OpenClaw hook URL" )
.option("--hook-token <token>" , "OpenClaw hook token" )
.option("--push-token <token>" , "Push token for gog watch serve" )
.option("--bind <host>" , "gog watch serve bind host" )
.option("--port <port>" , "gog watch serve port" )
.option("--path <path>" , "gog watch serve path" )
.option("--include-body" , "Include email body snippets" )
.option("--max-bytes <n>" , "Max bytes for body snippets" )
.option("--renew-minutes <n>" , "Renew watch every N minutes" )
.option("--tailscale <mode>" , "Expose push endpoint via tailscale (funnel|serve|off)" )
.option("--tailscale-path <path>" , "Path for tailscale serve/funnel" )
.option(
"--tailscale-target <target>" ,
"Tailscale serve/funnel target (port, host:port, or URL)" ,
)
.action(async (opts) => {
try {
const parsed = parseGmailRunOptions(opts);
await runGmailService(parsed);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1 );
}
});
}
function parseGmailSetupOptions(raw: Record<string, unknown>): GmailSetupOptions {
const accountRaw = raw.account;
const account = normalizeOptionalString(accountRaw) ?? "" ;
if (!account) {
throw new Error("--account is required" );
}
const common = parseGmailCommonOptions(raw);
return {
account,
project: normalizeOptionalString(raw.project),
...gmailOptionsFromCommon(common),
pushEndpoint: normalizeOptionalString(raw.pushEndpoint),
json: Boolean (raw.json),
};
}
function parseGmailRunOptions(raw: Record<string, unknown>): GmailRunOptions {
const common = parseGmailCommonOptions(raw);
return {
account: normalizeOptionalString(raw.account),
...gmailOptionsFromCommon(common),
};
}
function parseGmailCommonOptions(raw: Record<string, unknown>) {
return {
topic: normalizeOptionalString(raw.topic),
subscription: normalizeOptionalString(raw.subscription),
label: normalizeOptionalString(raw.label),
hookUrl: normalizeOptionalString(raw.hookUrl),
hookToken: normalizeOptionalString(raw.hookToken),
pushToken: normalizeOptionalString(raw.pushToken),
bind: normalizeOptionalString(raw.bind),
port: numberOption(raw.port),
path: normalizeOptionalString(raw.path),
includeBody: booleanOption(raw.includeBody),
maxBytes: numberOption(raw.maxBytes),
renewEveryMinutes: numberOption(raw.renewMinutes),
tailscaleRaw: normalizeOptionalString(raw.tailscale),
tailscalePath: normalizeOptionalString(raw.tailscalePath),
tailscaleTarget: normalizeOptionalString(raw.tailscaleTarget),
};
}
function gmailOptionsFromCommon(
common: ReturnType<typeof parseGmailCommonOptions>,
): Omit<GmailRunOptions, "account" > {
return {
topic: common.topic,
subscription: common.subscription,
label: common.label,
hookUrl: common.hookUrl,
hookToken: common.hookToken,
pushToken: common.pushToken,
bind: common.bind,
port: common.port,
path: common.path,
includeBody: common.includeBody,
maxBytes: common.maxBytes,
renewEveryMinutes: common.renewEveryMinutes,
tailscale: common.tailscaleRaw as GmailRunOptions["tailscale" ],
tailscalePath: common.tailscalePath,
tailscaleTarget: common.tailscaleTarget,
};
}
function numberOption(value: unknown): number | undefined {
if (value === undefined || value === null ) {
return undefined;
}
const n = typeof value === "number" ? value : Number(value);
if (!Number.isFinite(n) || n <= 0 ) {
return undefined;
}
return Math.floor(n);
}
function booleanOption(value: unknown): boolean | undefined {
if (value === undefined || value === null ) {
return undefined;
}
return Boolean (value);
}
Messung V0.5 in Prozent C=100 H=99 G=99
¤ Dauer der Verarbeitung: 0.9 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland