UNPKG

@gguf/claw

Version:

WhatsApp gateway CLI (Baileys web) with Pi RPC agent

1,228 lines (1,215 loc) 2.25 MB
import { A as getChildLogger, C as setVerbose, D as colorize, E as warn, F as CONFIG_PATH, L as STATE_DIR, O as isRich, P as normalizeLogLevel, Q as CHAT_CHANNEL_ORDER, T as success, X as resolveStateDir, a as parseBooleanValue$1, at as normalizeChannelId, ct as requireActivePluginRegistry, it as normalizeAnyChannelId, k as theme, lt as setActivePluginRegistry, n as isTruthyEnvValue, o as createSubsystemLogger, p as defaultRuntime, v as danger, w as shouldLogVerbose, x as logVerbose, y as info } from "./entry.js"; import { A as modelKey, D as buildModelAliasIndex, E as buildAllowedModelSet, F as resolveDefaultModelForAgent, L as resolveModelRefFromString, M as parseModelRef, P as resolveConfiguredModelRef, R as resolveThinkingDefault, T as resolveOpenClawAgentDir, V as normalizeGoogleModelId, _t as DEFAULT_MODEL, a as markAuthProfileUsed, at as resolveEnvApiKey, ct as getShellPathFromLoginShell, et as getApiKeyForModel, g as markAuthProfileGood, gt as DEFAULT_CONTEXT_TOKENS, h as listProfilesForProvider, i as markAuthProfileFailure, j as normalizeProviderId, k as isCliProvider, nt as requireApiKey, ot as resolveModelAuthMode, r as isProfileInCooldown, rt as resolveApiKeyForProvider, s as resolveApiKeyForProfile, t as resolveAuthProfileOrder, tt as getCustomProviderApiKey, ut as resolveShellEnvFallbackTimeoutMs, vt as DEFAULT_PROVIDER, w as resolveAuthStorePathForDisplay, y as ensureAuthProfileStore, yt as resolveAuthProfileDisplayLabel } from "./auth-profiles-CfFGCDJa.js"; import { t as formatCliCommand } from "./command-format-3xiXujG0.js"; import { A as buildAgentPeerSessionKey, B as isAcpSessionKey, C as loadWorkspaceBootstrapFiles, D as DEFAULT_AGENT_ID, E as DEFAULT_ACCOUNT_ID$1, F as resolveAgentIdFromSessionKey, H as parseAgentSessionKey, I as resolveThreadSessionKeys, L as sanitizeAgentId, M as normalizeAccountId$3, N as normalizeAgentId, O as DEFAULT_MAIN_KEY, P as normalizeMainKey, S as filterBootstrapFilesForSession, U as resolveThreadParentSessionKey, V as isSubagentSessionKey, c as resolveDefaultAgentId, f as DEFAULT_AGENT_WORKSPACE_DIR, i as resolveAgentModelFallbacksOverride, j as buildGroupHistoryKey, k as buildAgentMainSessionKey, l as resolveSessionAgentId, n as resolveAgentConfig, o as resolveAgentSkillsFilter, p as DEFAULT_BOOTSTRAP_FILENAME, r as resolveAgentDir, s as resolveAgentWorkspaceDir, t as listAgentIds, u as resolveSessionAgentIds, x as ensureAgentWorkspace } from "./agent-scope-jm0ZdXwM.js"; import { _ as sleep, b as truncateUtf16Safe, c as isSelfChatMode, g as shortenHomePath, h as shortenHomeInString, l as jidToE164, m as resolveUserPath, n as clampInt, p as resolveJidToE164, t as CONFIG_DIR, u as normalizeE164, v as sliceUtf16Safe, y as toWhatsappJid } from "./utils-PmTbZoD1.js"; import { a as logDebug, c as logWarn, i as spawnWithFallback, n as runExec, o as logError, r as formatSpawnError, s as logInfo, t as runCommandWithTimeout } from "./exec-BIMFe4XS.js"; import { t as resolveOpenClawPackageRoot } from "./openclaw-root-Dean7Fp2.js"; import { a as saveJsonFile, i as loadJsonFile } from "./github-copilot-token-rP-6QdKv.js"; import { i as discoverModels, r as discoverAuthStorage } from "./pi-model-discovery-CsRo-xMp.js"; import { A as resolveAgentMaxConcurrent, C as parseConfigPath, M as VERSION, S as getConfigValueAtPath, T as unsetConfigValueAtPath, a as readConfigFileSnapshot, b as setConfigOverride, d as TELEGRAM_COMMAND_NAME_PATTERN, f as normalizeTelegramCommandName, g as validateJsonSchemaValue, h as parseDurationMs, l as validateConfigObjectWithPlugins, m as isSafeExecutableValue, o as resolveConfigSnapshotHash, p as resolveTelegramCustomCommands, r as loadConfig, s as writeConfigFile, v as getConfigOverrides, w as setConfigValueAtPath, x as unsetConfigOverride, y as resetConfigOverrides } from "./config-DCT1RAo6.js"; import { c as resolveEnableState, l as resolveMemorySlotDecision, n as discoverOpenClawPlugins, s as normalizePluginsConfig, t as loadPluginManifestRegistry } from "./manifest-registry-tuAcHxrV.js"; import { a as resolveBrowserConfig } from "./server-context-CM_E6wD5.js"; import { E as DEFAULT_AI_SNAPSHOT_MAX_CHARS, n as formatErrorMessage, r as formatUncaughtError, t as extractErrorCode } from "./errors-DdT2Dtkb.js"; import { r as startBrowserControlServiceFromConfig, t as createBrowserControlContext } from "./control-service-Ds9ompnU.js"; import { t as pickPrimaryTailnetIPv4 } from "./tailnet-BijMqkqa.js"; import { Tt as SESSION_LABEL_MAX_LENGTH, t as GatewayClient } from "./client-cU7Xg1MO.js"; import { n as callGateway, r as randomIdempotencyKey } from "./call-CfqL-4Nc.js"; import { a as isInternalMessageChannel, c as listDeliverableMessageChannels, d as resolveMessageChannel, f as GATEWAY_CLIENT_IDS, l as normalizeMessageChannel, m as GATEWAY_CLIENT_NAMES, n as isDeliverableMessageChannel, o as isMarkdownCapableMessageChannel, p as GATEWAY_CLIENT_MODES, t as INTERNAL_MESSAGE_CHANNEL, u as resolveGatewayMessageChannel } from "./message-channel-CAFcg7mw.js"; import { t as formatDocsLink } from "./links-jGisPfXW.js"; import { _ as normalizeChatType, a as normalizeWhatsAppTarget, b as normalizeDiscordToken, c as resolveTelegramAccount, d as listBindings, g as resolveSlackBotToken, h as resolveSlackAppToken, i as isWhatsAppGroupJid, l as resolveTelegramToken, n as listChannelPlugins, o as listEnabledTelegramAccounts, p as resolveSlackAccount, r as normalizeChannelId$1, s as listTelegramAccountIds, t as getChannelPlugin, v as listEnabledDiscordAccounts, y as resolveDiscordAccount } from "./plugins-TrKFfrLt.js"; import { a as logoutWeb, d as webAuthExists, i as logWebSelfId, n as resolveWhatsAppAccount, r as getWebAuthAgeMs, s as readWebSelfId } from "./accounts-B5QZU96b.js"; import { n as withProgress, r as withProgressTotals } from "./progress-Dn3kWpaL.js"; import { r as stylePromptTitle } from "./prompt-style-D5D7b3cX.js"; import { i as resolveMemorySearchConfig, n as resolveRetryConfig, r as retryAsync } from "./manager-rDmdE7O9.js"; import { a as resolveSessionTranscriptsDirForAgent, n as resolveSessionFilePath, o as resolveStorePath, r as resolveSessionTranscriptPath } from "./paths-RvF0P6tQ.js"; import { t as emitSessionTranscriptUpdate } from "./transcript-events-Cj85Mq0h.js"; import { c as listMemoryFiles, l as normalizeExtraMemoryPaths } from "./sqlite-B_L84oiu.js"; import { C as mediaKindFromMime, _ as imageMimeFromFormat, b as kindFromMime, g as getFileExtension, h as extensionForMime, i as SsrFBlockedError, m as detectMime, n as getMediaDir, p as resizeToJpeg, r as saveMediaBuffer, u as getImageMetadata, v as isAudioFileName, x as MAX_IMAGE_BYTES, y as isGifMedia } from "./routes-yI5QIzeL.js"; import { A as resolveBootstrapMaxChars, B as normalizeVerboseLevel, C as isRawApiErrorPayload, D as sanitizeUserFacingText, E as parseImageSizeError, H as supportsXHighThinking, I as normalizeElevatedLevel, L as normalizeReasoningLevel, M as formatThinkingLevels, N as formatXHighModelHint, O as buildBootstrapContextFiles, R as normalizeThinkLevel, S as isRateLimitAssistantError, T as parseImageDimensionError, U as sanitizeImageBlocks, V as resolveResponseUsageMode, W as sanitizeToolResultImages, _ as isCompactionFailureError, a as isMessagingToolDuplicateNormalized, b as isFailoverErrorMessage, c as downgradeOpenAIReasoningBlocks, d as classifyFailoverReason, f as formatAssistantErrorText, g as isCloudCodeAssistFormatError, h as isAuthAssistantError, j as sanitizeGoogleTurnOrdering, k as ensureSessionHeader, l as isAntigravityClaude, m as getApiErrorPayloadFingerprint, n as validateGeminiTurns, o as normalizeTextForComparison, p as formatRawAssistantErrorForUi, r as pickFallbackThinkingLevel, s as sanitizeSessionMessagesImages, t as validateAnthropicTurns, u as isGoogleModelApi, v as isContextOverflowError, w as isTimeoutErrorMessage, x as isLikelyContextOverflowError, y as isFailoverAssistantError, z as normalizeUsageDisplay } from "./pi-embedded-helpers-DJgCXZEz.js"; import { A as chunkMarkdownIR, B as chunkMarkdownText, C as normalizeTargetForProvider, D as HEARTBEAT_TOKEN, E as parseInlineDirectives$1, F as MediaFetchError, G as resolveTextChunkLimit, H as chunkText, I as fetchRemoteMedia, J as parseFenceSpans, K as findFenceSpanAt, L as fetchWithSsrFGuard, M as markdownToIRWithMeta, N as loadWebMedia, O as SILENT_REPLY_TOKEN, P as loadWebMediaRaw, R as resolveMarkdownTableMode, S as normalizeChannelTargetInput, T as splitMediaFromOutput, U as chunkTextWithMode, V as chunkMarkdownTextWithMode, W as resolveChunkMode, _ as signalRpcRequest, a as normalizeReplyPayloadsForDelivery, b as wrapFetchWithAbortSignal, c as filterMessagingToolDuplicates, d as createReplyToModeFilterForChannel, f as resolveReplyToMode, g as signalCheck, h as sendTypingSignal, j as markdownToIR, k as isSilentReplyText, l as isRenderablePayload, m as sendReadReceiptSignal, o as applyReplyTagsToPayload, p as sendMessageSignal, q as isSafeFenceBreak, s as applyReplyThreading, t as deliverOutboundPayloads, u as shouldSuppressMessagingToolReplies, v as streamSignalEvents, w as parseReplyDirectives, x as buildTargetResolverSignature, y as resolveFetch, z as chunkByNewline } from "./deliver-eE21zdeQ.js"; import { A as resolveSessionResetType, B as resolveConversationLabel, C as normalizeDeliveryContext, D as evaluateSessionFreshness, E as resolveSessionKey$1, H as resolveGroupSessionKey, I as resolveMainSessionKey, J as expandPolicyWithPluginGroups, K as buildPluginToolGroups, M as DEFAULT_RESET_TRIGGERS, N as canonicalizeMainSessionAlias, O as resolveChannelResetConfig, Q as stripPluginOnlyAllowlist, R as deriveSessionMetaPatch, S as mergeDeliveryContext, U as resolveSandboxConfigForAgent, V as buildGroupDisplayName, X as normalizeToolName, Z as resolveToolProfilePolicy, _ as updateSessionStoreEntry, a as ensureSandboxWorkspaceForSession, at as resolveChannelGroupRequireMention, b as deliveryContextFromSession, c as resolveSandboxRuntimeStatus, ct as resolveSignalAccount, d as loadSessionStore, f as readSessionUpdatedAt, g as updateSessionStore, h as updateLastRoute, it as resolveChannelGroupPolicy, j as resolveThreadFlag, k as resolveSessionResetPolicy, l as appendAssistantMessageToSessionTranscript, lt as resolveIMessageAccount, nt as getChannelDock, o as resolveSandboxContext, p as recordSessionMetaFromInbound, q as collectExplicitAllowlist, rt as listChannelDocks, st as listEnabledSignalAccounts, v as isCacheEnabled, w as normalizeSessionDeliveryFields, x as deliveryContextKey, y as resolveCacheTtlMs$1 } from "./sandbox-Cnq9TXEn.js"; import { C as parseCommandArgs, D as shouldHandleTextCommands, E as serializeCommandArgs, S as normalizeCommandBody, T as resolveCommandArgMenu, _ as findCommandByNativeName, b as listNativeCommandSpecs, f as estimateUsageCost, g as buildCommandTextFromArgs, h as resolveModelCostConfig, m as formatUsd$1, o as extractTextFromMessage, p as formatTokenCount$2, t as buildChannelSummary, v as listChatCommands, w as resolveCommandArgChoices, x as listNativeCommandSpecsForConfig, y as listChatCommandsForConfig } from "./channel-summary-BkqO8zZ9.js"; import { i as getMachineDisplayName, r as createBrowserRouteDispatcher, t as isWSL } from "./wsl-DASmek7h.js"; import { a as resolveSkillsPromptForRun, i as loadWorkspaceSkillEntries, l as applySkillEnvOverrides, n as buildWorkspaceSkillCommandSpecs, r as buildWorkspaceSkillSnapshot, s as resolvePluginSkillDirs, u as applySkillEnvOverridesFromSnapshot } from "./skills-DtwGIkTI.js"; import { _ as ensureOpenClawModelsJson, a as resolveProviderVisionModelFromConfig, c as extractAssistantThinking, d as formatReasoningMessage, f as inferToolMetaFromArgs, g as stripThinkingTagsFromText, h as stripMinimaxToolCallXml, i as decodeDataUrl, l as extractThinkingFromTaggedStream, m as stripDowngradedToolCallText, n as coerceImageAssistantText, o as minimaxUnderstandImage, p as promoteThinkingTagsToBlocks, r as coerceImageModelConfig, s as extractAssistantText$1, t as describeImageWithModel, u as extractThinkingFromTaggedText } from "./image-CXg7Z0WD.js"; import { a as evaluateShellAllowlist, d as requiresExecApproval, f as resolveExecApprovals, h as resolveSafeBins, o as maxAsk, p as resolveExecApprovalsFromFile, s as minSecurity, t as addAllowlistEntry, u as recordAllowlistUse } from "./exec-approvals-WdYFyy5N.js"; import { a as canvasSnapshotTempPath, c as parseCameraClipPayload, d as buildNodeShellCommand, i as parseEnvPairs, l as parseCameraSnapPayload, n as screenRecordTempPath, o as parseCanvasSnapshotPayload, r as writeScreenRecordToFile, s as cameraTempPath, t as parseScreenRecordPayload, u as writeBase64ToFile } from "./nodes-screen-DOhGEibx.js"; import { n as redactSensitiveText } from "./redact-CDPAzwi8.js"; import { n as resolveToolDisplay } from "./tool-display-BMYWrp0L.js"; import { t as parseAbsoluteTimeMs } from "./parse-BB0Cqon8.js"; import { n as formatDoctorNonInteractiveHint, s as writeRestartSentinel } from "./restart-sentinel-DywisDen.js"; import { d as resolveGatewaySystemdServiceName, l as resolveGatewayLaunchAgentLabel } from "./constants-CLUi6T-M.js"; import { n as resolveMessageChannelSelection, t as listConfiguredMessageChannels } from "./channel-selection-BAwiO0li.js"; import { t as parseTimeoutMs } from "./parse-timeout-BIB19Upi.js"; import { a as isToolAllowedByPolicies, c as resolveSubagentToolPolicy, i as filterToolsByPolicy, n as resolveNativeCommandsEnabled, o as resolveEffectiveToolPolicy, r as resolveNativeSkillsEnabled, s as resolveGroupToolPolicy, t as isNativeCommandsExplicitlyDisabled } from "./commands-DMKDOFmC.js"; import { a as removeChannelAllowFromStoreEntry, c as listPairingChannels, i as readChannelAllowFromStore, o as upsertChannelPairingRequest, t as addChannelAllowFromStoreEntry } from "./pairing-store-DMex6WWe.js"; import { a as getStatusCode$1, i as formatError$1, n as waitForWebLogin, o as waitForWaConnection, r as createWaSocket, t as startWebLoginWithQr } from "./login-qr-sEcxw1_U.js"; import { i as withManager, t as formatErrorMessage$1 } from "./cli-utils-DFbPmfWB.js"; import { t as resolvePairingIdLabel } from "./pairing-labels-C6I3dD-m.js"; import { createRequire } from "node:module"; import { execSync, spawn, spawnSync } from "node:child_process"; import path from "node:path"; import process$1 from "node:process"; import os, { homedir, tmpdir } from "node:os"; import fs, { constants, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs"; import { inspect } from "node:util"; import fs$1 from "node:fs/promises"; import { fileURLToPath } from "node:url"; import { complete, completeSimple, streamSimple } from "@mariozechner/pi-ai"; import crypto, { randomUUID } from "node:crypto"; import { CURRENT_SESSION_VERSION, SessionManager, SettingsManager, codingTools, createAgentSession, createEditTool, createReadTool, createWriteTool, estimateTokens, readTool } from "@mariozechner/pi-coding-agent"; import { createServer } from "node:http"; import { Buffer as Buffer$1 } from "node:buffer"; import * as net$1 from "node:net"; import readline, { createInterface } from "node:readline"; import { Type } from "@sinclair/typebox"; import { cancel, isCancel } from "@clack/prompts"; import { createJiti } from "jiti"; import chokidar from "chokidar"; import { WebClient } from "@slack/web-api"; import { ProxyAgent, fetch as fetch$1 } from "undici"; import { EdgeTTS } from "node-edge-tts"; import { EventEmitter } from "node:events"; import { ApplicationCommandOptionType, ButtonStyle, ChannelType, PermissionFlagsBits, Routes } from "discord-api-types/v10"; import { Button, ChannelType as ChannelType$1, Client, Command, MessageCreateListener, MessageReactionAddListener, MessageReactionRemoveListener, MessageType, PresenceUpdateListener, RateLimitError, RequestClient, Row } from "@buape/carbon"; import { PollLayoutType } from "discord-api-types/payloads/v10"; import { API_CONSTANTS, Bot, GrammyError, HttpError, InputFile, webhookCallback } from "grammy"; import { setTimeout as setTimeout$1 } from "node:timers/promises"; import { DisconnectReason, downloadMediaMessage, extractMessageContent, getContentType, isJidGroup, normalizeMessageContent } from "@whiskeysockets/baileys"; import { GatewayIntents, GatewayPlugin } from "@buape/carbon/gateway"; import { messagingApi } from "@line/bot-sdk"; import SlackBolt from "@slack/bolt"; import { run, sequentialize } from "@grammyjs/runner"; import { apiThrottler } from "@grammyjs/transformer-throttler"; //#region src/channels/targets.ts function normalizeTargetId(kind, id) { return `${kind}:${id}`.toLowerCase(); } function buildMessagingTarget(kind, id, raw) { return { kind, id, raw, normalized: normalizeTargetId(kind, id) }; } function ensureTargetId(params) { if (!params.pattern.test(params.candidate)) throw new Error(params.errorMessage); return params.candidate; } function requireTargetKind(params) { const kindLabel = params.kind; if (!params.target) throw new Error(`${params.platform} ${kindLabel} id is required.`); if (params.target.kind !== params.kind) throw new Error(`${params.platform} ${kindLabel} id is required (use ${kindLabel}:<id>).`); return params.target.id; } //#endregion //#region src/slack/targets.ts function parseSlackTarget(raw, options = {}) { const trimmed = raw.trim(); if (!trimmed) return; const mentionMatch = trimmed.match(/^<@([A-Z0-9]+)>$/i); if (mentionMatch) return buildMessagingTarget("user", mentionMatch[1], trimmed); if (trimmed.startsWith("user:")) { const id = trimmed.slice(5).trim(); return id ? buildMessagingTarget("user", id, trimmed) : void 0; } if (trimmed.startsWith("channel:")) { const id = trimmed.slice(8).trim(); return id ? buildMessagingTarget("channel", id, trimmed) : void 0; } if (trimmed.startsWith("slack:")) { const id = trimmed.slice(6).trim(); return id ? buildMessagingTarget("user", id, trimmed) : void 0; } if (trimmed.startsWith("@")) return buildMessagingTarget("user", ensureTargetId({ candidate: trimmed.slice(1).trim(), pattern: /^[A-Z0-9]+$/i, errorMessage: "Slack DMs require a user id (use user:<id> or <@id>)" }), trimmed); if (trimmed.startsWith("#")) return buildMessagingTarget("channel", ensureTargetId({ candidate: trimmed.slice(1).trim(), pattern: /^[A-Z0-9]+$/i, errorMessage: "Slack channels require a channel id (use channel:<id>)" }), trimmed); if (options.defaultKind) return buildMessagingTarget(options.defaultKind, trimmed, trimmed); return buildMessagingTarget("channel", trimmed, trimmed); } function resolveSlackChannelId(raw) { return requireTargetKind({ platform: "Slack", target: parseSlackTarget(raw, { defaultKind: "channel" }), kind: "channel" }); } //#endregion //#region src/channels/channel-config.ts function applyChannelMatchMeta(result, match) { if (match.matchKey && match.matchSource) { result.matchKey = match.matchKey; result.matchSource = match.matchSource; } return result; } function resolveChannelMatchConfig(match, resolveEntry) { if (!match.entry) return null; return applyChannelMatchMeta(resolveEntry(match.entry), match); } function buildChannelKeyCandidates(...keys) { const seen = /* @__PURE__ */ new Set(); const candidates = []; for (const key of keys) { if (typeof key !== "string") continue; const trimmed = key.trim(); if (!trimmed || seen.has(trimmed)) continue; seen.add(trimmed); candidates.push(trimmed); } return candidates; } function resolveChannelEntryMatch(params) { const entries = params.entries ?? {}; const match = {}; for (const key of params.keys) { if (!Object.prototype.hasOwnProperty.call(entries, key)) continue; match.entry = entries[key]; match.key = key; break; } if (params.wildcardKey && Object.prototype.hasOwnProperty.call(entries, params.wildcardKey)) { match.wildcardEntry = entries[params.wildcardKey]; match.wildcardKey = params.wildcardKey; } return match; } function resolveChannelEntryMatchWithFallback(params) { const direct = resolveChannelEntryMatch({ entries: params.entries, keys: params.keys, wildcardKey: params.wildcardKey }); if (direct.entry && direct.key) return { ...direct, matchKey: direct.key, matchSource: "direct" }; const normalizeKey = params.normalizeKey; if (normalizeKey) { const normalizedKeys = params.keys.map((key) => normalizeKey(key)).filter(Boolean); if (normalizedKeys.length > 0) for (const [entryKey, entry] of Object.entries(params.entries ?? {})) { const normalizedEntry = normalizeKey(entryKey); if (normalizedEntry && normalizedKeys.includes(normalizedEntry)) return { ...direct, entry, key: entryKey, matchKey: entryKey, matchSource: "direct" }; } } const parentKeys = params.parentKeys ?? []; if (parentKeys.length > 0) { const parent = resolveChannelEntryMatch({ entries: params.entries, keys: parentKeys }); if (parent.entry && parent.key) return { ...direct, entry: parent.entry, key: parent.key, parentEntry: parent.entry, parentKey: parent.key, matchKey: parent.key, matchSource: "parent" }; if (normalizeKey) { const normalizedParentKeys = parentKeys.map((key) => normalizeKey(key)).filter(Boolean); if (normalizedParentKeys.length > 0) for (const [entryKey, entry] of Object.entries(params.entries ?? {})) { const normalizedEntry = normalizeKey(entryKey); if (normalizedEntry && normalizedParentKeys.includes(normalizedEntry)) return { ...direct, entry, key: entryKey, parentEntry: entry, parentKey: entryKey, matchKey: entryKey, matchSource: "parent" }; } } } if (direct.wildcardEntry && direct.wildcardKey) return { ...direct, entry: direct.wildcardEntry, key: direct.wildcardKey, matchKey: direct.wildcardKey, matchSource: "wildcard" }; return direct; } //#endregion //#region src/channels/allowlist-match.ts function formatAllowlistMatchMeta(match) { return `matchKey=${match?.matchKey ?? "none"} matchSource=${match?.matchSource ?? "none"}`; } //#endregion //#region src/plugins/commands.ts const pluginCommands = /* @__PURE__ */ new Map(); let registryLocked = false; const MAX_ARGS_LENGTH = 4096; /** * Reserved command names that plugins cannot override. * These are built-in commands from commands-registry.data.ts. */ const RESERVED_COMMANDS = new Set([ "help", "commands", "status", "whoami", "context", "stop", "restart", "reset", "new", "compact", "config", "debug", "allowlist", "activation", "skill", "subagents", "model", "models", "queue", "send", "bash", "exec", "think", "verbose", "reasoning", "elevated", "usage" ]); /** * Validate a command name. * Returns an error message if invalid, or null if valid. */ function validateCommandName(name) { const trimmed = name.trim().toLowerCase(); if (!trimmed) return "Command name cannot be empty"; if (!/^[a-z][a-z0-9_-]*$/.test(trimmed)) return "Command name must start with a letter and contain only letters, numbers, hyphens, and underscores"; if (RESERVED_COMMANDS.has(trimmed)) return `Command name "${trimmed}" is reserved by a built-in command`; return null; } /** * Register a plugin command. * Returns an error if the command name is invalid or reserved. */ function registerPluginCommand(pluginId, command) { if (registryLocked) return { ok: false, error: "Cannot register commands while processing is in progress" }; if (typeof command.handler !== "function") return { ok: false, error: "Command handler must be a function" }; const validationError = validateCommandName(command.name); if (validationError) return { ok: false, error: validationError }; const key = `/${command.name.toLowerCase()}`; if (pluginCommands.has(key)) { const existing = pluginCommands.get(key); return { ok: false, error: `Command "${command.name}" already registered by plugin "${existing.pluginId}"` }; } pluginCommands.set(key, { ...command, pluginId }); logVerbose(`Registered plugin command: ${key} (plugin: ${pluginId})`); return { ok: true }; } /** * Clear all registered plugin commands. * Called during plugin reload. */ function clearPluginCommands() { pluginCommands.clear(); } /** * Check if a command body matches a registered plugin command. * Returns the command definition and parsed args if matched. * * Note: If a command has `acceptsArgs: false` and the user provides arguments, * the command will not match. This allows the message to fall through to * built-in handlers or the agent. Document this behavior to plugin authors. */ function matchPluginCommand(commandBody) { const trimmed = commandBody.trim(); if (!trimmed.startsWith("/")) return null; const spaceIndex = trimmed.indexOf(" "); const commandName = spaceIndex === -1 ? trimmed : trimmed.slice(0, spaceIndex); const args = spaceIndex === -1 ? void 0 : trimmed.slice(spaceIndex + 1).trim(); const key = commandName.toLowerCase(); const command = pluginCommands.get(key); if (!command) return null; if (args && !command.acceptsArgs) return null; return { command, args: args || void 0 }; } /** * Sanitize command arguments to prevent injection attacks. * Removes control characters and enforces length limits. */ function sanitizeArgs(args) { if (!args) return; if (args.length > MAX_ARGS_LENGTH) return args.slice(0, MAX_ARGS_LENGTH); let sanitized = ""; for (const char of args) { const code = char.charCodeAt(0); if (!(code <= 31 && code !== 9 && code !== 10 || code === 127)) sanitized += char; } return sanitized; } /** * Execute a plugin command handler. * * Note: Plugin authors should still validate and sanitize ctx.args for their * specific use case. This function provides basic defense-in-depth sanitization. */ async function executePluginCommand(params) { const { command, args, senderId, channel, isAuthorizedSender, commandBody, config } = params; if (command.requireAuth !== false && !isAuthorizedSender) { logVerbose(`Plugin command /${command.name} blocked: unauthorized sender ${senderId || "<unknown>"}`); return { text: "⚠️ This command requires authorization." }; } const ctx = { senderId, channel, isAuthorizedSender, args: sanitizeArgs(args), commandBody, config }; registryLocked = true; try { const result = await command.handler(ctx); logVerbose(`Plugin command /${command.name} executed successfully for ${senderId || "unknown"}`); return result; } catch (err) { const error = err; logVerbose(`Plugin command /${command.name} error: ${error.message}`); return { text: "⚠️ Command failed. Please try again later." }; } finally { registryLocked = false; } } /** * List all registered plugin commands. * Used for /help and /commands output. */ function listPluginCommands() { return Array.from(pluginCommands.values()).map((cmd) => ({ name: cmd.name, description: cmd.description, pluginId: cmd.pluginId })); } /** * Get plugin command specs for native command registration (e.g., Telegram). */ function getPluginCommandSpecs() { return Array.from(pluginCommands.values()).map((cmd) => ({ name: cmd.name, description: cmd.description })); } //#endregion //#region src/plugins/hooks.ts /** * Get hooks for a specific hook name, sorted by priority (higher first). */ function getHooksForName(registry, hookName) { return registry.typedHooks.filter((h) => h.hookName === hookName).toSorted((a, b) => (b.priority ?? 0) - (a.priority ?? 0)); } /** * Create a hook runner for a specific registry. */ function createHookRunner(registry, options = {}) { const logger = options.logger; const catchErrors = options.catchErrors ?? true; /** * Run a hook that doesn't return a value (fire-and-forget style). * All handlers are executed in parallel for performance. */ async function runVoidHook(hookName, event, ctx) { const hooks = getHooksForName(registry, hookName); if (hooks.length === 0) return; logger?.debug?.(`[hooks] running ${hookName} (${hooks.length} handlers)`); const promises = hooks.map(async (hook) => { try { await hook.handler(event, ctx); } catch (err) { const msg = `[hooks] ${hookName} handler from ${hook.pluginId} failed: ${String(err)}`; if (catchErrors) logger?.error(msg); else throw new Error(msg, { cause: err }); } }); await Promise.all(promises); } /** * Run a hook that can return a modifying result. * Handlers are executed sequentially in priority order, and results are merged. */ async function runModifyingHook(hookName, event, ctx, mergeResults) { const hooks = getHooksForName(registry, hookName); if (hooks.length === 0) return; logger?.debug?.(`[hooks] running ${hookName} (${hooks.length} handlers, sequential)`); let result; for (const hook of hooks) try { const handlerResult = await hook.handler(event, ctx); if (handlerResult !== void 0 && handlerResult !== null) if (mergeResults && result !== void 0) result = mergeResults(result, handlerResult); else result = handlerResult; } catch (err) { const msg = `[hooks] ${hookName} handler from ${hook.pluginId} failed: ${String(err)}`; if (catchErrors) logger?.error(msg); else throw new Error(msg, { cause: err }); } return result; } /** * Run before_agent_start hook. * Allows plugins to inject context into the system prompt. * Runs sequentially, merging systemPrompt and prependContext from all handlers. */ async function runBeforeAgentStart(event, ctx) { return runModifyingHook("before_agent_start", event, ctx, (acc, next) => ({ systemPrompt: next.systemPrompt ?? acc?.systemPrompt, prependContext: acc?.prependContext && next.prependContext ? `${acc.prependContext}\n\n${next.prependContext}` : next.prependContext ?? acc?.prependContext })); } /** * Run agent_end hook. * Allows plugins to analyze completed conversations. * Runs in parallel (fire-and-forget). */ async function runAgentEnd(event, ctx) { return runVoidHook("agent_end", event, ctx); } /** * Run before_compaction hook. */ async function runBeforeCompaction(event, ctx) { return runVoidHook("before_compaction", event, ctx); } /** * Run after_compaction hook. */ async function runAfterCompaction(event, ctx) { return runVoidHook("after_compaction", event, ctx); } /** * Run message_received hook. * Runs in parallel (fire-and-forget). */ async function runMessageReceived(event, ctx) { return runVoidHook("message_received", event, ctx); } /** * Run message_sending hook. * Allows plugins to modify or cancel outgoing messages. * Runs sequentially. */ async function runMessageSending(event, ctx) { return runModifyingHook("message_sending", event, ctx, (acc, next) => ({ content: next.content ?? acc?.content, cancel: next.cancel ?? acc?.cancel })); } /** * Run message_sent hook. * Runs in parallel (fire-and-forget). */ async function runMessageSent(event, ctx) { return runVoidHook("message_sent", event, ctx); } /** * Run before_tool_call hook. * Allows plugins to modify or block tool calls. * Runs sequentially. */ async function runBeforeToolCall(event, ctx) { return runModifyingHook("before_tool_call", event, ctx, (acc, next) => ({ params: next.params ?? acc?.params, block: next.block ?? acc?.block, blockReason: next.blockReason ?? acc?.blockReason })); } /** * Run after_tool_call hook. * Runs in parallel (fire-and-forget). */ async function runAfterToolCall(event, ctx) { return runVoidHook("after_tool_call", event, ctx); } /** * Run tool_result_persist hook. * * This hook is intentionally synchronous: it runs in hot paths where session * transcripts are appended synchronously. * * Handlers are executed sequentially in priority order (higher first). Each * handler may return `{ message }` to replace the message passed to the next * handler. */ function runToolResultPersist(event, ctx) { const hooks = getHooksForName(registry, "tool_result_persist"); if (hooks.length === 0) return; let current = event.message; for (const hook of hooks) try { const out = hook.handler({ ...event, message: current }, ctx); if (out && typeof out.then === "function") { const msg = `[hooks] tool_result_persist handler from ${hook.pluginId} returned a Promise; this hook is synchronous and the result was ignored.`; if (catchErrors) { logger?.warn?.(msg); continue; } throw new Error(msg); } const next = out?.message; if (next) current = next; } catch (err) { const msg = `[hooks] tool_result_persist handler from ${hook.pluginId} failed: ${String(err)}`; if (catchErrors) logger?.error(msg); else throw new Error(msg, { cause: err }); } return { message: current }; } /** * Run session_start hook. * Runs in parallel (fire-and-forget). */ async function runSessionStart(event, ctx) { return runVoidHook("session_start", event, ctx); } /** * Run session_end hook. * Runs in parallel (fire-and-forget). */ async function runSessionEnd(event, ctx) { return runVoidHook("session_end", event, ctx); } /** * Run gateway_start hook. * Runs in parallel (fire-and-forget). */ async function runGatewayStart(event, ctx) { return runVoidHook("gateway_start", event, ctx); } /** * Run gateway_stop hook. * Runs in parallel (fire-and-forget). */ async function runGatewayStop(event, ctx) { return runVoidHook("gateway_stop", event, ctx); } /** * Check if any hooks are registered for a given hook name. */ function hasHooks(hookName) { return registry.typedHooks.some((h) => h.hookName === hookName); } /** * Get count of registered hooks for a given hook name. */ function getHookCount(hookName) { return registry.typedHooks.filter((h) => h.hookName === hookName).length; } return { runBeforeAgentStart, runAgentEnd, runBeforeCompaction, runAfterCompaction, runMessageReceived, runMessageSending, runMessageSent, runBeforeToolCall, runAfterToolCall, runToolResultPersist, runSessionStart, runSessionEnd, runGatewayStart, runGatewayStop, hasHooks, getHookCount }; } //#endregion //#region src/plugins/hook-runner-global.ts const log$11 = createSubsystemLogger("plugins"); let globalHookRunner = null; let globalRegistry = null; /** * Initialize the global hook runner with a plugin registry. * Called once when plugins are loaded during gateway startup. */ function initializeGlobalHookRunner(registry) { globalRegistry = registry; globalHookRunner = createHookRunner(registry, { logger: { debug: (msg) => log$11.debug(msg), warn: (msg) => log$11.warn(msg), error: (msg) => log$11.error(msg) }, catchErrors: true }); const hookCount = registry.hooks.length; if (hookCount > 0) log$11.info(`hook runner initialized with ${hookCount} registered hooks`); } /** * Get the global hook runner. * Returns null if plugins haven't been loaded yet. */ function getGlobalHookRunner() { return globalHookRunner; } //#endregion //#region src/hooks/internal-hooks.ts /** Registry of hook handlers by event key */ const handlers$1 = /* @__PURE__ */ new Map(); /** * Register a hook handler for a specific event type or event:action combination * * @param eventKey - Event type (e.g., 'command') or specific action (e.g., 'command:new') * @param handler - Function to call when the event is triggered * * @example * ```ts * // Listen to all command events * registerInternalHook('command', async (event) => { * console.log('Command:', event.action); * }); * * // Listen only to /new commands * registerInternalHook('command:new', async (event) => { * await saveSessionToMemory(event); * }); * ``` */ function registerInternalHook(eventKey, handler) { if (!handlers$1.has(eventKey)) handlers$1.set(eventKey, []); handlers$1.get(eventKey).push(handler); } /** * Clear all registered hooks (useful for testing) */ function clearInternalHooks() { handlers$1.clear(); } /** * Trigger a hook event * * Calls all handlers registered for: * 1. The general event type (e.g., 'command') * 2. The specific event:action combination (e.g., 'command:new') * * Handlers are called in registration order. Errors are caught and logged * but don't prevent other handlers from running. * * @param event - The event to trigger */ async function triggerInternalHook(event) { const typeHandlers = handlers$1.get(event.type) ?? []; const specificHandlers = handlers$1.get(`${event.type}:${event.action}`) ?? []; const allHandlers = [...typeHandlers, ...specificHandlers]; if (allHandlers.length === 0) return; for (const handler of allHandlers) try { await handler(event); } catch (err) { console.error(`Hook error [${event.type}:${event.action}]:`, err instanceof Error ? err.message : String(err)); } } /** * Create a hook event with common fields filled in * * @param type - The event type * @param action - The action within that type * @param sessionKey - The session key * @param context - Additional context */ function createInternalHookEvent(type, action, sessionKey, context = {}) { return { type, action, sessionKey, context, timestamp: /* @__PURE__ */ new Date(), messages: [] }; } //#endregion //#region src/plugins/http-path.ts function normalizePluginHttpPath(path, fallback) { const trimmed = path?.trim(); if (!trimmed) { const fallbackTrimmed = fallback?.trim(); if (!fallbackTrimmed) return null; return fallbackTrimmed.startsWith("/") ? fallbackTrimmed : `/${fallbackTrimmed}`; } return trimmed.startsWith("/") ? trimmed : `/${trimmed}`; } //#endregion //#region src/plugins/registry.ts function createPluginRegistry(registryParams) { const registry = { plugins: [], tools: [], hooks: [], typedHooks: [], channels: [], providers: [], gatewayHandlers: {}, httpHandlers: [], httpRoutes: [], cliRegistrars: [], services: [], commands: [], diagnostics: [] }; const coreGatewayMethods = new Set(Object.keys(registryParams.coreGatewayHandlers ?? {})); const pushDiagnostic = (diag) => { registry.diagnostics.push(diag); }; const registerTool = (record, tool, opts) => { const names = opts?.names ?? (opts?.name ? [opts.name] : []); const optional = opts?.optional === true; const factory = typeof tool === "function" ? tool : (_ctx) => tool; if (typeof tool !== "function") names.push(tool.name); const normalized = names.map((name) => name.trim()).filter(Boolean); if (normalized.length > 0) record.toolNames.push(...normalized); registry.tools.push({ pluginId: record.id, factory, names: normalized, optional, source: record.source }); }; const registerHook = (record, events, handler, opts, config) => { const normalizedEvents = (Array.isArray(events) ? events : [events]).map((event) => event.trim()).filter(Boolean); const entry = opts?.entry ?? null; const name = entry?.hook.name ?? opts?.name?.trim(); if (!name) { pushDiagnostic({ level: "warn", pluginId: record.id, source: record.source, message: "hook registration missing name" }); return; } const description = entry?.hook.description ?? opts?.description ?? ""; const hookEntry = entry ? { ...entry, hook: { ...entry.hook, name, description, source: "openclaw-plugin", pluginId: record.id }, metadata: { ...entry.metadata, events: normalizedEvents } } : { hook: { name, description, source: "openclaw-plugin", pluginId: record.id, filePath: record.source, baseDir: path.dirname(record.source), handlerPath: record.source }, frontmatter: {}, metadata: { events: normalizedEvents }, invocation: { enabled: true } }; record.hookNames.push(name); registry.hooks.push({ pluginId: record.id, entry: hookEntry, events: normalizedEvents, source: record.source }); if (!(config?.hooks?.internal?.enabled === true) || opts?.register === false) return; for (const event of normalizedEvents) registerInternalHook(event, handler); }; const registerGatewayMethod = (record, method, handler) => { const trimmed = method.trim(); if (!trimmed) return; if (coreGatewayMethods.has(trimmed) || registry.gatewayHandlers[trimmed]) { pushDiagnostic({ level: "error", pluginId: record.id, source: record.source, message: `gateway method already registered: ${trimmed}` }); return; } registry.gatewayHandlers[trimmed] = handler; record.gatewayMethods.push(trimmed); }; const registerHttpHandler = (record, handler) => { record.httpHandlers += 1; registry.httpHandlers.push({ pluginId: record.id, handler, source: record.source }); }; const registerHttpRoute = (record, params) => { const normalizedPath = normalizePluginHttpPath(params.path); if (!normalizedPath) { pushDiagnostic({ level: "warn", pluginId: record.id, source: record.source, message: "http route registration missing path" }); return; } if (registry.httpRoutes.some((entry) => entry.path === normalizedPath)) { pushDiagnostic({ level: "error", pluginId: record.id, source: record.source, message: `http route already registered: ${normalizedPath}` }); return; } record.httpHandlers += 1; registry.httpRoutes.push({ pluginId: record.id, path: normalizedPath, handler: params.handler, source: record.source }); }; const registerChannel = (record, registration) => { const normalized = typeof registration.plugin === "object" ? registration : { plugin: registration }; const plugin = normalized.plugin; const id = typeof plugin?.id === "string" ? plugin.id.trim() : String(plugin?.id ?? "").trim(); if (!id) { pushDiagnostic({ level: "error", pluginId: record.id, source: record.source, message: "channel registration missing id" }); return; } record.channelIds.push(id); registry.channels.push({ pluginId: record.id, plugin, dock: normalized.dock, source: record.source }); }; const registerProvider = (record, provider) => { const id = typeof provider?.id === "string" ? provider.id.trim() : ""; if (!id) { pushDiagnostic({ level: "error", pluginId: record.id, source: record.source, message: "provider registration missing id" }); return; } const existing = registry.providers.find((entry) => entry.provider.id === id); if (existing) { pushDiagnostic({ level: "error", pluginId: record.id, source: record.source, message: `provider already registered: ${id} (${existing.pluginId})` }); return; } record.providerIds.push(id); registry.providers.push({ pluginId: record.id, provider, source: record.source }); }; const registerCli = (record, registrar, opts) => { const commands = (opts?.commands ?? []).map((cmd) => cmd.trim()).filter(Boolean); record.cliCommands.push(...commands); registry.cliRegistrars.push({ pluginId: record.id, register: registrar, commands, source: record.source }); }; const registerService = (record, service) => { const id = service.id.trim(); if (!id) return; record.services.push(id); registry.services.push({ pluginId: record.id, service, source: record.source }); }; const registerCommand = (record, command) => { const name = command.name.trim(); if (!name) { pushDiagnostic({ level: "error", pluginId: record.id, source: record.source, message: "command registration missing name" }); return; } const result = registerPluginCommand(record.id, command); if (!result.ok) { pushDiagnostic({ level: "error", pluginId: record.id, source: record.source, message: `command registration failed: ${result.error}` }); return; } record.commands.push(name); registry.commands.push({ pluginId: record.id, command, source: record.source }); }; const registerTypedHook = (record, hookName, handler, opts) => { record.hookCount += 1; registry.typedHooks.push({ pluginId: record.id, hookName, handler, priority: opts?.priority, source: record.source }); }; const normalizeLogger = (logger) => ({ info: logger.info, warn: logger.warn, error: logger.error, debug: logger.debug }); const createApi = (record, params) => { return { id: record.id, name: record.name, version: record.version, description: record.description, source: record.source, config: params.config, pluginConfig: params.pluginConfig, runtime: registryParams.runtime, logger: normalizeLogger(registryParams.logger), registerTool: (tool, opts) => registerTool(record, tool, opts), registerHook: (events, handler, opts) => registerHook(record, events, handler, opts, params.config), registerHttpHandler: (handler) => registerHttpHandler(record, handler), registerHttpRoute: (params) => registerHttpRoute(record, params), registerChannel: (registration) => registerChannel(record, registration), registerProvider: (provider) => registerProvider(record, provider), registerGatewayMethod: (method, handler) => registerGatewayMethod(record, method, handler), registerCli: (registrar, opts) => registerCli(record, registrar, opts), registerService: (service) => registerService(record, service), registerCommand: (command) => registerCommand(record, command), resolvePath: (input) => resolveUserPath(input), on: (hookName, handler, opts) => registerTypedHook(record, hookName, handler, opts) }; }; return { registry, createApi, pushDiagnostic, registerTool, registerChannel, registerProvider, registerGatewayMethod, registerCli, registerService, registerCommand, registerHook, registerTypedHook }; } //#endregion //#region src/agents/identity.ts const DEFAULT_ACK_REACTION = "👀"; function resolveAgentIdentity(cfg, agentId) { return resolveAgentConfig(cfg, agentId)?.identity; } function resolveAckReaction(cfg, agentId) { const configured = cfg.messages?.ackReaction; if (configured !== void 0) return configured.trim(); return resolveAgentIdentity(cfg, agentId)?.emoji?.trim() || DEFAULT_ACK_REACTION; } function resolveIdentityNamePrefix(cfg, agentId) { const name = resolveAgentIdentity(cfg, agentId)?.name?.trim(); if (!name) return; return `[${name}]`; } /** Returns just the identity name (without brackets) for template context. */ function resolveIdentityName(cfg, agentId) { return resolveAgentIdentity(cfg, agentId)?.name?.trim() || void 0; } function resolveMessagePrefix(cfg, agentId, opts) { const configured = opts?.configured ?? cfg.messages?.messagePrefix; if (configured !== void 0) return configured; if (opts?.hasAllowFrom === true) return ""; return resolveIdentityNamePrefix(cfg, agentId) ?? opts?.fallback ?? "[openclaw]"; } function resolveResponsePrefix(cfg, agentId) { const configured = cfg.messages?.responsePrefix; if (configured !== void 0) { if (configured === "auto") return resolveIdentityNamePrefix(cfg, agentId); return configured; } } function resolveEffectiveMessagesConfig(cfg, agentId, opts) { return { messagePrefix: resolveMessagePrefix(cfg, agentId, { hasAllowFrom: opts?.hasAllowFrom, fallback: opts?.fallbackMessagePrefix }), responsePrefix: resolveResponsePrefix(cfg, agentId) }; } function resolveHumanDelayConfig(cfg, agentId) { const defaults = cfg.agents?.defaults?.humanDelay; const overrides = resolveAgentConfig(cfg, agentId)?.humanDelay; if (!defaults && !overrides) return; return { mode: overrides?.mode ?? defaults?.mode, minMs: overrides?.minMs ?? defaults?.minMs, maxMs: overrides?.maxMs ?? defaults?.maxMs }; } //#endregion //#region src/utils/shell-argv.ts function splitShellArgs(raw) { const tokens = []; let buf = ""; let inSingle = false; let inDouble = false; let escaped = false; const pushToken = () => { if (buf.length > 0) { tokens.push(buf); buf = ""; } }; for (let i = 0; i < raw.length; i += 1) { const ch = raw[i]; if (escaped) { buf += ch; escaped = false; continue; } if (!inSingle && !inDouble && ch === "\\") { escaped = true; continue; } if (inSingle) { if (ch === "'") inSingle = false; else buf += ch; continue; } if (inDouble) { if (ch === "\"") inDouble = false; else buf += ch; continue; } if (ch === "'") { inSingle = true; continue; } if (ch === "\"") { inDouble = true; continue; } if (/\s/.test(ch)) { pushToken(); continue; } buf += ch; } if (escaped || inSingle || inDouble) return null; pushToken(); return tokens; } //#endregion //#region src/memory/backend-config.ts const DEFAULT_BACKEND = "builtin"; const DEFAULT_CITATIONS = "auto"; const DEFAULT_QMD_INTERVAL = "5m"; const DEFAULT_QMD_DEBOUNCE_MS = 15e3; const DEFAULT_QMD_TIMEOUT_MS = 4e3; const DEFAULT_QMD_EMBED_INTERVAL = "60m"; const DEFAULT_QMD_LIMITS = { maxResults: 6, maxSnippetChars: 700, maxInjectedChars: 4e3, timeoutMs: DEFAULT_QMD_TIMEOUT_MS }; const DEFAULT_QMD_SCOPE = { default: "deny", rules: [{ action: "allow", match: { chatType: "direct" } }] }; function sanitizeName(input) { return input.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "collection"; } function ensureUniqueName(base, existing) { let name = sanitizeName(base); if (!existing.has(name)) { existing.add(name); return name; } let suffix = 2; while (existing.has(`${name}-${suffix}`)) suffix += 1; const unique = `${name}-${suffix}`; existing.add(unique); return unique; } function resolvePath(raw, workspaceDir) { const trimmed = raw.trim(); if (!trimmed) throw new Error("path required"); if (trimmed.startsWith("~") || path.isAbsolute(trimmed)) return path.normalize(resolveUserPath(trimmed)); return path.normalize(path.resolve(workspaceDir, trimmed)); } function resolveIntervalMs(raw) { const value = raw?.trim(); if (!value) return parseDurationMs(DEFAULT_QMD_INTERVAL, { defaultUnit: "m" }); try { return parseDurationMs(value, { de