Files
clawdbot/src/cli/hooks-cli.ts
2026-01-04 14:38:51 +00:00

183 lines
6.2 KiB
TypeScript

import type { Command } from "commander";
import { danger } from "../globals.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 {
type GmailRunOptions,
type GmailSetupOptions,
runGmailService,
runGmailSetup,
} from "../hooks/gmail-ops.js";
import { defaultRuntime } from "../runtime.js";
export function registerHooksCli(program: Command) {
const hooks = program
.command("hooks")
.description("Webhook helpers and hook-based integrations");
const gmail = hooks
.command("gmail")
.description("Gmail Pub/Sub hooks (via gogcli)");
gmail
.command("setup")
.description("Configure Gmail watch + Pub/Sub + Clawdbot 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>", "Clawdbot hook URL")
.option("--hook-token <token>", "Clawdbot 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("--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>", "Clawdbot hook URL")
.option("--hook-token <token>", "Clawdbot 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")
.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 = typeof accountRaw === "string" ? accountRaw.trim() : "";
if (!account) throw new Error("--account is required");
return {
account,
project: stringOption(raw.project),
topic: stringOption(raw.topic),
subscription: stringOption(raw.subscription),
label: stringOption(raw.label),
hookUrl: stringOption(raw.hookUrl),
hookToken: stringOption(raw.hookToken),
pushToken: stringOption(raw.pushToken),
bind: stringOption(raw.bind),
port: numberOption(raw.port),
path: stringOption(raw.path),
includeBody: booleanOption(raw.includeBody),
maxBytes: numberOption(raw.maxBytes),
renewEveryMinutes: numberOption(raw.renewMinutes),
tailscale: stringOption(raw.tailscale) as GmailSetupOptions["tailscale"],
tailscalePath: stringOption(raw.tailscalePath),
pushEndpoint: stringOption(raw.pushEndpoint),
json: Boolean(raw.json),
};
}
function parseGmailRunOptions(raw: Record<string, unknown>): GmailRunOptions {
return {
account: stringOption(raw.account),
topic: stringOption(raw.topic),
subscription: stringOption(raw.subscription),
label: stringOption(raw.label),
hookUrl: stringOption(raw.hookUrl),
hookToken: stringOption(raw.hookToken),
pushToken: stringOption(raw.pushToken),
bind: stringOption(raw.bind),
port: numberOption(raw.port),
path: stringOption(raw.path),
includeBody: booleanOption(raw.includeBody),
maxBytes: numberOption(raw.maxBytes),
renewEveryMinutes: numberOption(raw.renewMinutes),
tailscale: stringOption(raw.tailscale) as GmailRunOptions["tailscale"],
tailscalePath: stringOption(raw.tailscalePath),
};
}
function stringOption(value: unknown): string | undefined {
if (typeof value !== "string") return undefined;
const trimmed = value.trim();
return trimmed ? trimmed : undefined;
}
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);
}