Files
clawdbot/src/index.ts
2025-11-25 03:22:18 +01:00

880 lines
23 KiB
JavaScript

#!/usr/bin/env node
import crypto from "node:crypto";
import fs from "node:fs";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import process, { stdin as input, stdout as output } from "node:process";
import readline from "node:readline/promises";
import { fileURLToPath } from "node:url";
import chalk from "chalk";
import dotenv from "dotenv";
import JSON5 from "json5";
import Twilio from "twilio";
import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message.js";
import type { TwilioSenderListClient, TwilioRequester } from "./twilio/types.js";
import {
runCommandWithTimeout,
runExec,
type SpawnResult,
} from "./process/exec.js";
import { defaultRuntime, type RuntimeEnv } from "./runtime.js";
import { sendTypingIndicator } from "./twilio/typing.js";
import {
autoReplyIfConfigured,
getReplyFromConfig,
} from "./auto-reply/reply.js";
import { readEnv, ensureTwilioEnv, type EnvConfig } from "./env.js";
import { createClient } from "./twilio/client.js";
import { logTwilioSendError, formatTwilioError } from "./twilio/utils.js";
import { monitorTwilio as monitorTwilioImpl } from "./twilio/monitor.js";
import { sendMessage, waitForFinalStatus } from "./twilio/send.js";
import { startWebhook as startWebhookImpl } from "./twilio/webhook.js";
import {
updateWebhook as updateWebhookImpl,
findIncomingNumberSid as findIncomingNumberSidImpl,
findMessagingServiceSid as findMessagingServiceSidImpl,
setMessagingServiceWebhook as setMessagingServiceWebhookImpl,
} from "./twilio/update-webhook.js";
import {
listRecentMessages,
formatMessageLine,
uniqueBySid,
sortByDateDesc,
} from "./twilio/messages.js";
import { CLAUDE_BIN, parseClaudeJsonText } from "./auto-reply/claude.js";
import {
applyTemplate,
type MsgContext,
type TemplateContext,
} from "./auto-reply/templating.js";
import {
CONFIG_PATH,
type WarelayConfig,
type SessionConfig,
type SessionScope,
type ReplyMode,
type ClaudeOutputFormat,
loadConfig,
} from "./config/config.js";
import { sendCommand } from "./commands/send.js";
import { statusCommand } from "./commands/status.js";
import { upCommand } from "./commands/up.js";
import { webhookCommand } from "./commands/webhook.js";
import {
danger,
info,
isVerbose,
isYes,
logVerbose,
setVerbose,
setYes,
success,
warn,
} from "./globals.js";
import {
loginWeb,
monitorWebInbox,
sendMessageWeb,
WA_WEB_AUTH_DIR,
webAuthExists,
} from "./provider-web.js";
import type { Provider } from "./utils.js";
import {
assertProvider,
CONFIG_DIR,
jidToE164,
normalizeE164,
normalizePath,
sleep,
toWhatsappJid,
withWhatsAppPrefix,
} from "./utils.js";
import {
DEFAULT_IDLE_MINUTES,
DEFAULT_RESET_TRIGGER,
deriveSessionKey,
loadSessionStore,
resolveStorePath,
saveSessionStore,
SESSION_STORE_DEFAULT,
} from "./config/sessions.js";
dotenv.config({ quiet: true });
import { buildProgram } from "./cli/program.js";
const program = buildProgram();
type CliDeps = {
sendMessage: typeof sendMessage;
sendMessageWeb: typeof sendMessageWeb;
waitForFinalStatus: typeof waitForFinalStatus;
assertProvider: typeof assertProvider;
createClient?: typeof createClient;
monitorTwilio: typeof monitorTwilio;
listRecentMessages: typeof listRecentMessages;
ensurePortAvailable: typeof ensurePortAvailable;
startWebhook: typeof startWebhook;
waitForever: typeof waitForever;
ensureBinary: typeof ensureBinary;
ensureFunnel: typeof ensureFunnel;
getTailnetHostname: typeof getTailnetHostname;
readEnv: typeof readEnv;
findWhatsappSenderSid: typeof findWhatsappSenderSid;
updateWebhook: typeof updateWebhook;
handlePortError: typeof handlePortError;
monitorWebProvider: typeof monitorWebProvider;
};
function createDefaultDeps(): CliDeps {
return {
sendMessage,
sendMessageWeb,
waitForFinalStatus,
assertProvider,
createClient,
monitorTwilio,
listRecentMessages,
ensurePortAvailable,
startWebhook,
waitForever,
ensureBinary,
ensureFunnel,
getTailnetHostname,
readEnv,
findWhatsappSenderSid,
updateWebhook,
handlePortError,
monitorWebProvider,
};
}
class PortInUseError extends Error {
port: number;
details?: string;
constructor(port: number, details?: string) {
super(`Port ${port} is already in use.`);
this.name = "PortInUseError";
this.port = port;
this.details = details;
}
}
function isErrno(err: unknown): err is NodeJS.ErrnoException {
return Boolean(err && typeof err === "object" && "code" in err);
}
async function describePortOwner(port: number): Promise<string | undefined> {
// Best-effort process info for a listening port (macOS/Linux).
try {
const { stdout } = await runExec("lsof", [
"-i",
`tcp:${port}`,
"-sTCP:LISTEN",
"-nP",
]);
const trimmed = stdout.trim();
if (trimmed) return trimmed;
} catch (err) {
logVerbose(`lsof unavailable: ${String(err)}`);
}
return undefined;
}
async function ensurePortAvailable(port: number): Promise<void> {
// Detect EADDRINUSE early with a friendly message.
try {
await new Promise<void>((resolve, reject) => {
const tester = net
.createServer()
.once("error", (err) => reject(err))
.once("listening", () => {
tester.close(() => resolve());
})
.listen(port);
});
} catch (err) {
if (isErrno(err) && err.code === "EADDRINUSE") {
const details = await describePortOwner(port);
throw new PortInUseError(port, details);
}
throw err;
}
}
async function handlePortError(
err: unknown,
port: number,
context: string,
runtime: RuntimeEnv = defaultRuntime,
): Promise<never> {
if (
err instanceof PortInUseError ||
(isErrno(err) && err.code === "EADDRINUSE")
) {
const details =
err instanceof PortInUseError
? err.details
: await describePortOwner(port);
runtime.error(danger(`${context} failed: port ${port} is already in use.`));
if (details) {
runtime.error(info("Port listener details:"));
runtime.error(details);
if (/warelay|src\/index\.ts|dist\/index\.js/.test(details)) {
runtime.error(
warn(
"It looks like another warelay instance is already running. Stop it or pick a different port.",
),
);
}
}
runtime.error(
info(
"Resolve by stopping the process using the port or passing --port <free-port>.",
),
);
runtime.exit(1);
}
runtime.error(danger(`${context} failed: ${String(err)}`));
return runtime.exit(1);
}
async function ensureBinary(
name: string,
exec: typeof runExec = runExec,
runtime: RuntimeEnv = defaultRuntime,
): Promise<void> {
// Abort early if a required CLI tool is missing.
await exec("which", [name]).catch(() => {
runtime.error(`Missing required binary: ${name}. Please install it.`);
runtime.exit(1);
});
}
async function promptYesNo(
question: string,
defaultYes = false,
): Promise<boolean> {
if (isVerbose() && isYes()) return true; // redundant guard when both flags set
if (isYes()) return true;
const rl = readline.createInterface({ input, output });
const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
const answer = (await rl.question(`${question}${suffix}`))
.trim()
.toLowerCase();
rl.close();
if (!answer) return defaultYes;
return answer.startsWith("y");
}
function createClient(env: EnvConfig) {
// Twilio client using either auth token or API key/secret.
if ("authToken" in env.auth) {
return Twilio(env.accountSid, env.auth.authToken, {
accountSid: env.accountSid,
});
}
return Twilio(env.auth.apiKey, env.auth.apiSecret, {
accountSid: env.accountSid,
});
}
// sendMessage / waitForFinalStatus now live in src/twilio/send.ts and are imported above.
// startWebhook now lives in src/twilio/webhook.ts; keep shim for existing imports/tests.
async function startWebhook(
port: number,
path = "/webhook/whatsapp",
autoReply: string | undefined,
verbose: boolean,
runtime: RuntimeEnv = defaultRuntime,
): Promise<import("http").Server> {
return startWebhookImpl(port, path, autoReply, verbose, runtime);
}
function waitForever() {
// Keep event loop alive via an unref'ed interval plus a pending promise.
const interval = setInterval(() => {}, 1_000_000);
interval.unref();
return new Promise<void>(() => {
/* never resolve */
});
}
async function getTailnetHostname(exec: typeof runExec = runExec) {
// Derive tailnet hostname (or IP fallback) from tailscale status JSON.
const { stdout } = await exec("tailscale", ["status", "--json"]);
const parsed = stdout ? (JSON.parse(stdout) as Record<string, unknown>) : {};
const self =
typeof parsed.Self === "object" && parsed.Self !== null
? (parsed.Self as Record<string, unknown>)
: undefined;
const dns =
typeof self?.DNSName === "string" ? (self.DNSName as string) : undefined;
const ips = Array.isArray(self?.TailscaleIPs)
? (self.TailscaleIPs as string[])
: [];
if (dns && dns.length > 0) return dns.replace(/\.$/, "");
if (ips.length > 0) return ips[0];
throw new Error("Could not determine Tailscale DNS or IP");
}
async function ensureGoInstalled(
exec: typeof runExec = runExec,
prompt: typeof promptYesNo = promptYesNo,
runtime: RuntimeEnv = defaultRuntime,
) {
// Ensure Go toolchain is present; offer Homebrew install if missing.
const hasGo = await exec("go", ["version"]).then(
() => true,
() => false,
);
if (hasGo) return;
const install = await prompt(
"Go is not installed. Install via Homebrew (brew install go)?",
true,
);
if (!install) {
runtime.error("Go is required to build tailscaled from source. Aborting.");
runtime.exit(1);
}
logVerbose("Installing Go via Homebrew…");
await exec("brew", ["install", "go"]);
}
async function ensureTailscaledInstalled(
exec: typeof runExec = runExec,
prompt: typeof promptYesNo = promptYesNo,
runtime: RuntimeEnv = defaultRuntime,
) {
// Ensure tailscaled binary exists; install via Homebrew tailscale if missing.
const hasTailscaled = await exec("tailscaled", ["--version"]).then(
() => true,
() => false,
);
if (hasTailscaled) return;
const install = await prompt(
"tailscaled not found. Install via Homebrew (tailscale package)?",
true,
);
if (!install) {
runtime.error("tailscaled is required for user-space funnel. Aborting.");
runtime.exit(1);
}
logVerbose("Installing tailscaled via Homebrew…");
await exec("brew", ["install", "tailscale"]);
}
async function ensureFunnel(
port: number,
exec: typeof runExec = runExec,
runtime: RuntimeEnv = defaultRuntime,
prompt: typeof promptYesNo = promptYesNo,
) {
// Ensure Funnel is enabled and publish the webhook port.
try {
const statusOut = (
await exec("tailscale", ["funnel", "status", "--json"])
).stdout.trim();
const parsed = statusOut
? (JSON.parse(statusOut) as Record<string, unknown>)
: {};
if (!parsed || Object.keys(parsed).length === 0) {
runtime.error(
danger("Tailscale Funnel is not enabled on this tailnet/device."),
);
runtime.error(
info(
"Enable in admin console: https://login.tailscale.com/admin (see https://tailscale.com/kb/1223/funnel)",
),
);
runtime.error(
info(
"macOS user-space tailscaled docs: https://github.com/tailscale/tailscale/wiki/Tailscaled-on-macOS",
),
);
const proceed = await prompt(
"Attempt local setup with user-space tailscaled?",
true,
);
if (!proceed) runtime.exit(1);
await ensureGoInstalled(exec, prompt, runtime);
await ensureTailscaledInstalled(exec, prompt, runtime);
}
logVerbose(`Enabling funnel on port ${port}`);
const { stdout } = await exec(
"tailscale",
["funnel", "--yes", "--bg", `${port}`],
{
maxBuffer: 200_000,
timeoutMs: 15_000,
},
);
if (stdout.trim()) console.log(stdout.trim());
} catch (err) {
const errOutput = err as { stdout?: unknown; stderr?: unknown };
const stdout = typeof errOutput.stdout === "string" ? errOutput.stdout : "";
const stderr = typeof errOutput.stderr === "string" ? errOutput.stderr : "";
if (stdout.includes("Funnel is not enabled")) {
console.error(danger("Funnel is not enabled on this tailnet/device."));
const linkMatch = stdout.match(/https?:\/\/\S+/);
if (linkMatch) {
console.error(info(`Enable it here: ${linkMatch[0]}`));
} else {
console.error(
info(
"Enable in admin console: https://login.tailscale.com/admin (see https://tailscale.com/kb/1223/funnel)",
),
);
}
}
if (
stderr.includes("client version") ||
stdout.includes("client version")
) {
console.error(
warn(
"Tailscale client/server version mismatch detected; try updating tailscale/tailscaled.",
),
);
}
runtime.error(
"Failed to enable Tailscale Funnel. Is it allowed on your tailnet?",
);
runtime.error(
info(
"Tip: you can fall back to polling (no webhooks needed): `pnpm warelay relay --provider twilio --interval 5 --lookback 10`",
),
);
if (isVerbose()) {
if (stdout.trim()) runtime.error(chalk.gray(`stdout: ${stdout.trim()}`));
if (stderr.trim()) runtime.error(chalk.gray(`stderr: ${stderr.trim()}`));
runtime.error(err as Error);
}
runtime.exit(1);
}
}
async function findWhatsappSenderSid(
client: ReturnType<typeof createClient>,
from: string,
explicitSenderSid?: string,
runtime: RuntimeEnv = defaultRuntime,
) {
// Use explicit sender SID if provided, otherwise list and match by sender_id.
if (explicitSenderSid) {
logVerbose(`Using TWILIO_SENDER_SID from env: ${explicitSenderSid}`);
return explicitSenderSid;
}
try {
// Prefer official SDK list helper to avoid request-shape mismatches.
// Twilio helper types are broad; we narrow to expected shape.
const senderClient = client as unknown as TwilioSenderListClient;
const senders = await senderClient.messaging.v2.channelsSenders.list({
channel: "whatsapp",
pageSize: 50,
});
if (!senders) {
throw new Error('List senders response missing "senders" array');
}
const match = senders.find(
(s) =>
(typeof s.senderId === "string" &&
s.senderId === withWhatsAppPrefix(from)) ||
(typeof s.sender_id === "string" &&
s.sender_id === withWhatsAppPrefix(from)),
);
if (!match || typeof match.sid !== "string") {
throw new Error(
`Could not find sender ${withWhatsAppPrefix(from)} in Twilio account`,
);
}
return match.sid;
} catch (err) {
runtime.error(danger("Unable to list WhatsApp senders via Twilio API."));
if (isVerbose()) {
runtime.error(err as Error);
}
runtime.error(
info(
"Set TWILIO_SENDER_SID in .env to skip discovery (Twilio Console → Messaging → Senders → WhatsApp).",
),
);
runtime.exit(1);
}
}
async function setMessagingServiceWebhook(
client: TwilioSenderListClient,
url: string,
method: "POST" | "GET" = "POST",
): Promise<boolean> {
return setMessagingServiceWebhookImpl(client, url, method);
}
async function updateWebhook(
client: ReturnType<typeof createClient>,
senderSid: string,
url: string,
method: "POST" | "GET" = "POST",
runtime: RuntimeEnv = defaultRuntime,
) {
return updateWebhookImpl(client, senderSid, url, method, runtime);
}
function ensureTwilioEnv(runtime: RuntimeEnv = defaultRuntime) {
const required = ["TWILIO_ACCOUNT_SID", "TWILIO_WHATSAPP_FROM"];
const missing = required.filter((k) => !process.env[k]);
const hasToken = Boolean(process.env.TWILIO_AUTH_TOKEN);
const hasKey = Boolean(
process.env.TWILIO_API_KEY && process.env.TWILIO_API_SECRET,
);
if (missing.length > 0 || (!hasToken && !hasKey)) {
runtime.error(
danger(
`Missing Twilio env: ${missing.join(", ") || "auth token or api key/secret"}. Set them in .env before using provider=twilio.`,
),
);
runtime.exit(1);
}
}
async function pickProvider(pref: Provider | "auto"): Promise<Provider> {
if (pref !== "auto") return pref;
const hasWeb = await webAuthExists();
if (hasWeb) return "web";
return "twilio";
}
function readWebSelfId() {
const credsPath = path.join(WA_WEB_AUTH_DIR, "creds.json");
try {
if (!fs.existsSync(credsPath)) {
return { e164: null, jid: null };
}
const raw = fs.readFileSync(credsPath, "utf-8");
const parsed = JSON.parse(raw) as { me?: { id?: string } } | undefined;
const jid = parsed?.me?.id ?? null;
const e164 = jid ? jidToE164(jid) : null;
return { e164, jid };
} catch {
return { e164: null, jid: null };
}
}
function logWebSelfId(runtime: RuntimeEnv = defaultRuntime) {
const { e164, jid } = readWebSelfId();
const details =
e164 || jid
? `${e164 ?? "unknown"}${jid ? ` (jid ${jid})` : ""}`
: "unknown";
runtime.log(info(`Listening on web session: ${details}`));
}
function logTwilioFrom(runtime: RuntimeEnv = defaultRuntime) {
const env = readEnv(runtime);
runtime.log(
info(`Provider: twilio (polling inbound) | from ${env.whatsappFrom}`),
);
}
async function monitorTwilio(
intervalSeconds: number,
lookbackMinutes: number,
clientOverride?: ReturnType<typeof createClient>,
maxIterations = Infinity,
) {
// Delegate to the refactored monitor in src/twilio/monitor.ts.
return monitorTwilioImpl(
intervalSeconds,
lookbackMinutes,
{
client: clientOverride,
maxIterations,
deps: {
autoReplyIfConfigured,
listRecentMessages,
readEnv,
createClient,
sleep,
},
runtime: defaultRuntime,
},
);
}
async function monitorWebProvider(
verbose: boolean,
listenerFactory = monitorWebInbox,
keepAlive = true,
replyResolver: typeof getReplyFromConfig = getReplyFromConfig,
) {
// Listen for inbound personal WhatsApp Web messages and auto-reply if configured.
const listener = await listenerFactory({
verbose,
onMessage: async (msg) => {
const ts = msg.timestamp
? new Date(msg.timestamp).toISOString()
: new Date().toISOString();
console.log(`\n[${ts}] ${msg.from} -> ${msg.to}: ${msg.body}`);
const replyText = await replyResolver(
{
Body: msg.body,
From: msg.from,
To: msg.to,
MessageSid: msg.id,
},
{
onReplyStart: msg.sendComposing,
},
);
if (!replyText) return;
try {
await msg.reply(replyText);
if (isVerbose()) {
console.log(success(`↩️ Auto-replied to ${msg.from} (web)`));
}
} catch (err) {
console.error(
danger(
`Failed sending web auto-reply to ${msg.from}: ${String(err)}`,
),
);
}
},
});
console.log(
info(
"📡 Listening for personal WhatsApp Web inbound messages. Leave this running; Ctrl+C to stop.",
),
);
process.on("SIGINT", () => {
void listener.close().finally(() => {
console.log("\n👋 Web monitor stopped");
defaultRuntime.exit(0);
});
});
if (keepAlive) {
await waitForever();
}
}
async function performSend(
opts: {
to: string;
message: string;
wait: string;
poll: string;
provider: Provider;
},
deps: CliDeps,
_exitFn: (code: number) => never = defaultRuntime.exit,
_runtime: RuntimeEnv = defaultRuntime,
) {
deps.assertProvider(opts.provider);
const waitSeconds = Number.parseInt(opts.wait, 10);
const pollSeconds = Number.parseInt(opts.poll, 10);
if (Number.isNaN(waitSeconds) || waitSeconds < 0) {
throw new Error("Wait must be >= 0 seconds");
}
if (Number.isNaN(pollSeconds) || pollSeconds <= 0) {
throw new Error("Poll must be > 0 seconds");
}
if (opts.provider === "web") {
if (waitSeconds !== 0) {
console.log(info("Wait/poll are Twilio-only; ignored for provider=web."));
}
await deps.sendMessageWeb(opts.to, opts.message, { verbose: isVerbose() });
return;
}
const result = await deps.sendMessage(opts.to, opts.message, runtime);
if (!result) return;
if (waitSeconds === 0) return;
await deps.waitForFinalStatus(
result.client,
result.sid,
waitSeconds,
pollSeconds,
);
}
async function performStatus(
opts: { limit: string; lookback: string; json?: boolean },
deps: CliDeps,
_exitFn: (code: number) => never = defaultRuntime.exit,
_runtime: RuntimeEnv = defaultRuntime,
) {
const limit = Number.parseInt(opts.limit, 10);
const lookbackMinutes = Number.parseInt(opts.lookback, 10);
if (Number.isNaN(limit) || limit <= 0 || limit > 200) {
throw new Error("limit must be between 1 and 200");
}
if (Number.isNaN(lookbackMinutes) || lookbackMinutes <= 0) {
throw new Error("lookback must be > 0 minutes");
}
const messages = await deps.listRecentMessages(lookbackMinutes, limit);
if (opts.json) {
console.log(JSON.stringify(messages, null, 2));
return;
}
if (messages.length === 0) {
console.log("No messages found in the requested window.");
return;
}
for (const m of messages) {
console.log(formatMessageLine(m));
}
}
async function performWebhookSetup(
opts: {
port: string;
path: string;
reply?: string;
verbose?: boolean;
},
deps: CliDeps,
_exitFn: (code: number) => never = defaultRuntime.exit,
_runtime: RuntimeEnv = defaultRuntime,
) {
const port = Number.parseInt(opts.port, 10);
if (Number.isNaN(port) || port <= 0 || port >= 65536) {
throw new Error("Port must be between 1 and 65535");
}
await deps.ensurePortAvailable(port);
const server = await deps.startWebhook(
port,
opts.path,
opts.reply,
Boolean(opts.verbose),
);
return server;
}
async function performUp(
opts: {
port: string;
path: string;
verbose?: boolean;
yes?: boolean;
},
deps: CliDeps,
_exitFn: (code: number) => never = defaultRuntime.exit,
_runtime: RuntimeEnv = defaultRuntime,
) {
const port = Number.parseInt(opts.port, 10);
if (Number.isNaN(port) || port <= 0 || port >= 65536) {
throw new Error("Port must be between 1 and 65535");
}
await deps.ensurePortAvailable(port);
// Validate env and binaries
const env = deps.readEnv(runtime);
await deps.ensureBinary("tailscale", runExec, runtime);
// Enable Funnel first so we don't keep a webhook running on failure
await deps.ensureFunnel(port, runExec, runtime, promptYesNo);
const host = await deps.getTailnetHostname(runExec);
const publicUrl = `https://${host}${opts.path}`;
console.log(`🌐 Public webhook URL (via Funnel): ${publicUrl}`);
// Start webhook locally (after funnel success)
const server = await deps.startWebhook(
port,
opts.path,
undefined,
Boolean(opts.verbose),
);
// Configure Twilio sender webhook
const client = createClient(env);
const senderSid = await deps.findWhatsappSenderSid(
client,
env.whatsappFrom,
env.whatsappSenderSid,
);
await deps.updateWebhook(client, senderSid, publicUrl, "POST", runtime);
console.log(
"\nSetup complete. Leave this process running to keep the webhook online. Ctrl+C to stop.",
);
return { server, publicUrl, senderSid };
}
export {
assertProvider,
autoReplyIfConfigured,
applyTemplate,
createClient,
deriveSessionKey,
describePortOwner,
ensureBinary,
ensureFunnel,
ensureGoInstalled,
ensurePortAvailable,
ensureTailscaledInstalled,
findIncomingNumberSidImpl as findIncomingNumberSid,
findMessagingServiceSidImpl as findMessagingServiceSid,
findWhatsappSenderSid,
formatMessageLine,
formatTwilioError,
getReplyFromConfig,
getTailnetHostname,
handlePortError,
logTwilioSendError,
listRecentMessages,
loadConfig,
loadSessionStore,
monitorTwilio,
monitorWebProvider,
normalizeE164,
PortInUseError,
promptYesNo,
createDefaultDeps,
performSend,
performStatus,
performUp,
performWebhookSetup,
readEnv,
resolveStorePath,
runCommandWithTimeout,
runExec,
saveSessionStore,
sendMessage,
sendTypingIndicator,
setMessagingServiceWebhook,
sortByDateDesc,
startWebhook,
updateWebhook,
uniqueBySid,
waitForFinalStatus,
waitForever,
toWhatsappJid,
program,
};
const isMain =
process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
if (isMain) {
program.parseAsync(process.argv);
}