import * as Lark from '@larksuiteoapi/node-sdk'; import type { BackendType } from './adapters/backend/types.js'; import { type MojoConfig } from './adapters/backend/mojo-types.js'; import type { RiffBackendConfig } from './adapters/backend/riff-backend.js'; import type { CliId } from './adapters/cli/types.js'; import { type CliRuntimeConfig } from './adapters/cli/runtime.js'; import { type Locale } from './i18n/index.js'; import type { VoiceConfig } from './services/voice/types.js'; import { type Brand } from './im/lark/lark-hosts.js'; import type { BotSkillPolicy } from './core/skills/types.js'; import { type BotsConfigProvenance } from './core/config-dir.js'; import { type ExistingAppServerConfig } from './core/existing-app-server.js'; import type { FeedbackPolicy, FeedbackPolicyInput } from './services/feedback-policy.js'; import type { FeedbackWebhookDestination } from './services/feedback-outbox.js'; import { type SessionOwnerReminderConfig } from './core/session-owner-reminder.js'; import type { VcMeetingConsumerConfig, VcMeetingConsumerProfileConfig } from './types.js'; /** * Thrown when any Feishu client is requested for a core-only (`apiOnly`) bot. * Defined here (the lowest-level module that owns bot config + client) so both * `getBotClient` and higher layers (im/lark/client.ts primitives, doc-comment) * throw the SAME typed error without an import cycle. apiOnly = zero Feishu * network (reads AND writes); reaching a client request is genuine misuse. */ export declare class LarkTransportDisabledError extends Error { constructor(larkAppId: string, op: string); } export type { VcMeetingConsumerAgentConfig, VcMeetingConsumerConfig, VcMeetingConsumerManagedSink, VcMeetingConsumerProfileConfig, } from './types.js'; /** Bound every official-SDK HTTP call so one stalled provider request cannot * hold a bot-turn admission or maintenance mutation indefinitely. */ export declare const LARK_REQUEST_TIMEOUT_MS = 15000; /** Media uploads (image/file) ride the same official-SDK path but move real * bytes: a 30 MB video on a modest uplink legitimately exceeds the interactive * request bound. They also run in the `botmux send` CLI subprocess, which holds * no daemon admission/mutation lock, so the interactive timeout's protective * purpose does not apply to them. Give uploads a far looser ceiling. */ export declare const LARK_UPLOAD_TIMEOUT_MS = 120000; /** * Upper bound for a per-bot dsh runner turn timeout. Node's `setTimeout` delay * is a 32-bit signed int of milliseconds; a larger value silently wraps to ~1ms * and emits `TimeoutOverflowWarning`, so any timeout the runner will actually * arm must fit here. Config parsing, the dashboard IPC, and the dashboard UI all * validate against this single bound. */ export declare const MAX_TURN_TIMEOUT_MS = 2147483647; /** * Normalize an untrusted `turnTimeoutMs` value: a positive integer within the * arm-able bound is kept, anything else (≤0, non-integer, over the bound, * non-number, absent) collapses to `undefined` (= use the runner default). */ export declare function normalizeTurnTimeoutMs(value: unknown): number | undefined; /** * Normalize an untrusted `dshRuntime` value: only the two known variants are * kept; anything else (typo, unknown string, wrong type) collapses to * `undefined` (= official runner). The field is dsh-only; non-dsh CLIs drop * it at the call site (same pattern as turnTimeoutMs). */ export declare function normalizeDshRuntime(value: unknown): 'official' | 'tui' | undefined; export declare function configureLarkClientHttpTimeout(client: unknown): void; export declare function larkUploadHttpInstance(): unknown; export type ChatReplyMode = 'chat' | 'new-topic' | 'shared' | 'chat-topic'; /** Where a bot shows native Context / Token usage on its Session cards. */ export type UsageDisplayMode = 'streaming' | 'footer' | 'off'; /** Default when a bot sets nothing: usage rides the live streaming card. */ export declare const DEFAULT_USAGE_DISPLAY: UsageDisplayMode; export type ContentTriggerScope = 'topic' | 'regularGroup' | 'both'; export type ContentTriggerMatchType = 'keyword' | 'regex'; export type ContentTriggerActionType = 'start-or-wake-session'; export type MessageListenerSenderType = 'user' | 'bot'; export interface MessageListenerConfig { enabled: boolean; name?: string; replyCardTitle?: string; workingDir?: string; prompt: string; senderPolicy?: { /** * all_except_excluded: listen to all matching sender types except excluded ids. * include_only: listen only to includeSenderOpenIds; empty include means none. */ mode?: 'all_except_excluded' | 'include_only'; includeSenderOpenIds?: string[]; excludeSenderOpenIds?: string[]; /** * Persisted sender KIND for each exclude id (open_id → 'user' | 'bot'), so * the runtime fail-close decision (all_except_excluded + unverified bot * sender) can tell a muted human from a muted bot WITHOUT guessing by id * prefix. Absent entries fall back to a conservative "maybe a bot". */ excludeSenderKinds?: Record; includeSenderTypes?: MessageListenerSenderType[]; excludeSenderTypes?: MessageListenerSenderType[]; /** Default true. */ excludeSelf?: boolean; }; messagePolicy?: { /** Defaults to text + post. */ includeMsgTypes?: string[]; /** V1 only supports top-level group messages. */ scope?: 'top_level'; }; replyPolicy?: { /** V1 always replies under the triggering message. */ mode?: 'thread'; /** V1 starts one session per matched message. */ sessionMode?: 'per_message'; }; } export interface SummaryRangeConfig { /** 0 means no count limit; omitted defaults to 50. */ limit?: number; /** 0 means no time limit; omitted defaults to 24 hours. */ sinceHours?: number; } export interface ContentTriggerConfig { name: string; enabled: boolean; scope: ContentTriggerScope; /** * Default false. When true, this trigger may be matched by non-@ messages * authored by other bots. The current bot's own messages are still ignored. */ allowBotMessages?: boolean; match: { type: ContentTriggerMatchType; pattern: string; caseSensitive: boolean; }; history: { topic: { mode: 'current-thread'; }; regularGroup: { mode: 'recent-messages'; /** 0 means no count limit; omitted defaults to 50. */ limit?: number; /** 0 means no time limit; omitted means no time limit. */ sinceHours?: number; }; }; action: { type: ContentTriggerActionType; prompt: string; }; } export declare function normalizeVcMeetingConsumerProfiles(raw: unknown): VcMeetingConsumerProfileConfig[]; export type VcMeetingConsumerProfileResolution = { ok: true; source: 'legacy'; profiles: readonly []; selectedProfiles: readonly []; } | { ok: true; source: 'profiles'; profiles: readonly VcMeetingConsumerProfileConfig[]; selectedProfiles: readonly VcMeetingConsumerProfileConfig[]; } | { ok: false; source: 'legacy' | 'profiles'; errors: string[]; }; /** * Resolve and validate a profile-mode selection. The daemon can call this again * for card selections; config parsing calls it for the default selection. * Legacy configs deliberately return `source: legacy` without synthesizing * profiles so the existing dynamic-candidate/single-select path stays intact. */ export declare function resolveVcMeetingConsumerProfiles(config: VcMeetingConsumerConfig, selectedConsumerIds?: readonly string[]): VcMeetingConsumerProfileResolution; /** * A bots.json row may omit `cliId`. Historically that means claude-code, and such * a row can still carry a legacy `cliPathOverride`. Anything that derives a * selection from a RAW row must apply this same default, otherwise the selection * looks changed and preservation logic is skipped. Exported so there is exactly * one definition of the legacy default. */ export declare const LEGACY_DEFAULT_CLI_ID = "claude-code"; export interface OncallChat { /** Lark chat_id (oc_xxx) the bot was pulled into. */ chatId: string; /** Default working directory used for every new topic spawned in this chat. */ workingDir: string; } /** * Per-bot default for new group chats: * - `enabled` — when true, group chats first observed after `since` are * auto-bound to oncall on their first new-topic. * - `workingDir` — the working directory used for the auto-bind. Required * when enabled (oncall semantics: chatId ↔ workingDir). * - `since` — epoch ms when the flag was switched on. Used to gate * "new vs old" against chat-first-seen-store. Chats that * existed before `since` are left untouched, matching * "新群聊生效,老群聊不变". */ export interface BotDefaultOncall { enabled: boolean; workingDir: string; since: number; } export interface SubstituteTarget { /** App-scoped open_id. Directly comparable with Lark mention payloads. */ openId?: string; /** Tenant user_id. Preferred for hand-authored config when available. */ userId?: string; /** Tenant-stable union_id. Used when Lark includes it in mention payloads. */ unionId?: string; /** Reserved for a later resolver pass; v1 preserves it but does not match on it. */ email?: string; /** Human-readable label for prompt disclosure. */ name?: string; /** Cached avatar URL so the dashboard can show the resolved person's picture. */ avatarUrl?: string; } export interface SubstituteModeConfig { enabled: boolean; targets: SubstituteTarget[]; /** prefix = disclose "I will answer on behalf of X"; none = no extra disclosure instruction. */ disclosure?: 'prefix' | 'none'; /** Optional allow-list of chat IDs. When provided, substitute trigger only fires in these chats. */ chats?: string[]; /** Optional block-list of chat IDs (黑名单). When a chat is listed here the substitute * trigger never fires there — deny-wins over {@link chats} (a chat in both is blocked) * and hard (cannot be re-enabled by the per-chat `/substitute on` runtime toggle). * Applies to regular and topic groups alike. Direct @bot mentions are unaffected. */ excludedChats?: string[]; /** When true, do not automatically DM the owner a control card for substitute-mode sessions. */ disableControlCard?: boolean; /** How the bot replies to a substitute-mode trigger: * - 'thread' (default): reply in a Lark thread under the trigger message. * - 'quote': quote-reply the trigger message without creating a new topic. */ replyMode?: 'thread' | 'quote'; /** 话题群支持:在话题群(chat_mode=topic)里也响应替身触发。替身回合沿话题 * 路由进该话题自己的会话(无会话则新开),与普通群「进群 chat-scope 会话」 * 同构。缺省 true;显式 false 关闭。 */ topicGroups?: boolean; /** 话题里已有本 bot 活跃会话时是否仍触发替身(替身回合注入该会话)。false 时 * 回落到原让路行为(@别人=转交别人,保持沉默)。仅话题群路径生效,缺省 true。 */ topicActiveSessionTrigger?: boolean; } export interface VcMeetingAgentConfig { enabled?: boolean; /** Existing chat used for meeting transcript/chat sync. If unset, confirmation creates a listener group. */ listenerChatId?: string; notificationChatId?: string; attentionTargetOpenId?: string; larkCliProfile?: string; /** IANA time zone used when rendering listener-group timestamps. Defaults to Asia/Shanghai. */ timeZone?: string; /** Pending invite confirmation TTL. Defaults to 30 minutes. */ inviteTtlMs?: number; /** Transcript stability window before listener-group sync emits a sentence. */ stabilizeMs?: number; /** Listener-group sync interval. */ flushIntervalMs?: number; /** Realtime voice v0. Disabled by default; requires realtime scope and meeting-side AI speaking permission. */ realtimeVoice?: VcMeetingRealtimeVoiceConfig; /** Optional listener-group consumer. Card choices are driven entirely by this bots.json block. */ meetingConsumer?: VcMeetingConsumerConfig; } export interface VcMeetingRealtimeVoiceConfig { /** * Enables realtime voice. This opens the meeting realtime WebSocket after bot * join; without vc:meeting.bot.realtime:write or meeting-side speaking * permission it fail-closes with an explicit warning and never sends audio. */ enabled?: boolean; /** Expected PCM sample rate for session.create, default 24000. */ sampleRate?: number; /** Expected PCM channel count for session.create, default mono. */ channels?: number; /** Upstream PCM frame duration, default 100ms (4800B at 24kHz mono s16le). */ frameMs?: number; /** M0 dogfood only: speak this text once after realtime session.created. */ testSpeakOnStartText?: string; } /** * Per-bot settings for p2pMode='group' session groups (each top-level DM * message births a dedicated 1-user+1-bot group hosting the conversation). * Everything is optional; effective defaults in parentheses. */ export interface SessionGroupConfig { /** Group-name generation. */ naming?: { /** * 'ai-summary' (default): create with a truncated placeholder name, then * asynchronously ask the bot's own CLI (one-shot headless call) for a * short title and rename the chat when it lands. Falls back to the * placeholder on failure/timeout. * 'truncate': placeholder only — zero cost, zero delay. */ mode?: 'ai-summary' | 'truncate'; /** Max title length in characters for the AI summary (12). */ maxLen?: number; }; /** * Optional fixed group-name prefix. Empty/undefined (default) = no prefix. * Only needed as the match key for the rule-based feed-group mode (PR2). */ namePrefix?: string; /** Template working dir bound to each new session group (defaultWorkingDir). */ workingDir?: string; /** Send a DM receipt linking the freshly-created group (true). */ dmReceipt?: boolean; /** * What to do with the group when its session is closed: * 'keep' (default) — leave the group and registry entry; a later message in * the group resumes the closed session (same-group resume). 'disband' / * 'archive' are reserved for a follow-up PR and currently behave as 'keep'. */ onClose?: 'keep' | 'disband' | 'archive'; /** * Session-group tagging. * 'feed-group' (default) — the owner's personal sidebar 消息分组 (feed * group). Needs a one-time user OAuth (im:feed_group_v1), auto-refreshed * afterwards; works on any tenant — no tenant scope catalog involved. * 'chat-tag' — tenant chat tags (企业自定义群标签): a property of the GROUP * itself, applied with the bot's own tenant token. Zero user OAuth; needs * the im:tag:write + im:biz_entity_tag_relation:write tenant scopes, which * some tenants' scope catalogs don't offer at all (hence not the default). * 'off' — no tagging. */ tag?: { mode?: 'chat-tag' | 'feed-group' | 'off'; /** Tag / feed-group display name (default: Botmux群会话). */ name?: string; }; /** * Distinctive built-in group avatar for session groups — the zero-permission * visual marker (works on tenants without the chat-tag catalog). * 'auto' (default) applies it at birth; 'off' keeps Feishu's default avatar. */ avatar?: 'auto' | 'off'; /** Reserved (PR3): auto-dispose after N idle days; 0/undefined = off. */ idleDays?: number; } export interface BotConfig { larkAppId: string; larkAppSecret: string; /** * Core-only / headless 模式:该 bot 纯 HTTP 控制 API 驱动(trigger → * spawn → CLI → trigger-result),**不连接任何飞书**——boot 时跳过 * open_id 探测、required-scope 校验、WSClient 事件订阅,也不投递飞书消息 * (异步控制回路本就在 `deliverFinalOutput` 的 async 分支 early-return, * 运行时不触达飞书)。`larkAppId` 仍必填但用合成本地身份(如 * `local_`,非 `cli_` 前缀)作为 daemon 标识 + dashboard 路由 key + * `/api/trigger` 的 cachedLarkAppId gate;`larkAppSecret` 在此模式下可缺省。 * 缺省 / false 保持原有飞书 bot 行为字节不变。 */ apiOnly?: boolean; /** Final-answer feedback policy. Missing/disabled is intentionally inert. */ feedback?: FeedbackPolicyInput | FeedbackPolicy; /** Per-chat final-answer feedback overrides, scoped to this bot app id. */ chatFeedbackPolicies?: Record; feedbackWebhooks?: { destinations: FeedbackWebhookDestination[]; }; /** * 租户品牌:`'feishu'`(中国版,open.feishu.cn)或 `'lark'`(国际版, * open.larksuite.com)。缺省 / 旧 bots.json 无此字段 → 视为 `'feishu'` * (见 {@link normalizeBrand}),向后兼容。决定 SDK Client / WSClient 的 * domain、所有裸 fetch 的 host、OAuth / applink 深链等——全部从这一个字段 * 派生(见 im/lark/lark-hosts.ts)。setup 时自动识别后落盘;brand 绑定到 * 具体 app/租户,不在运行时切换(要换平台 = 重新配/加一个 bot)。 */ brand?: Brand; /** Optional process-name suffix; the daemon's process name is rendered as `botmux-` (defaults to `botmux-`). */ name?: string; /** * 自定义展示名(备注名)。设置后 dashboard 全站(名册 / 会话列表 / 各 bot * 下拉)用它替代飞书探测到的应用名展示;未设置则跟随飞书名称。纯展示字段: * 不影响 pm2 进程名(那是 {@link name})、不改飞书群内显示的应用名(开放 * 平台无改名 API,只能在开发者后台改)、也不进跨 bot @ 路由的 bots-info * 名册。可从 dashboard Bot Defaults 页或 `/config displayName` 修改,热更新。 */ displayName?: string; cliId: CliId; /** * Optional distribution identity for a CLI that is protocol-compatible with * {@link cliId} but ships as an independent executable/release stream (for * example a Codex-compatible fork). The adapter remains selected by cliId; * this descriptor owns product identity, executable and update provenance. * * `cliPathOverride` remains readable for legacy configs. A configured runtime * is exposed through cliPathOverride in memory as a compatibility shadow so * existing adapter call sites keep launching the selected executable while * the runtime rollout migrates them to the structured descriptor. */ cliRuntime?: CliRuntimeConfig; /** @deprecated Prefer cliRuntime.executable for newly configured runtimes. */ cliPathOverride?: string; /** * 通用启动前缀(按空格拆 token):worker spawn 时把启动命令拼成 * ` `(首 token 当 bin 走 PATH 解析),无需 wrapper 脚本、跨系统。 * 典型值 `"aiden x claude"` / `"aiden x codex"`(企业网关 + SSO),也能 * 承载 ccr / claude-w 等任意启动器。`cliId` 仍是底层适配器(claude→claude-code、 * codex→codex),所有适配器机制(hook / bridge / resume)照常工作;设了 wrapperCli 后 * 它的首 token 取代 cliId 的默认 bin(cliPathOverride 不再生效)。检测到前缀是 * `aiden x claude` 时自动剥掉 aiden 拒收的 --settings。见 src/setup/cli-selection.ts。 */ wrapperCli?: string; /** * Per-bot launch-shell override for the persistent backends (tmux/zellij/zmx). * When set, botmux launches the CLI under this shell instead of the daemon's * `$SHELL`. Accepts a bare name (`zsh`/`bash`/`fish`/`sh`) or an absolute path * (`/usr/bin/fish`). The escape hatch for a login `$SHELL` (e.g. bash) whose * rcfile `exec`-trampolines into another shell: that trampoline replaces the * launch shell before it can `exec` the CLI, leaving a bare shell the first * prompt gets typed into (`zsh: parse error`). Pinning `launchShell: fish` * launches under fish directly and bypasses the bash `.bashrc`. CAVEAT: * PATH/nvm/pnpm shims must then live in the pinned shell's rcfiles (for * example `.zshrc`/`.zprofile` or `~/.config/fish/config.fish`), not the bypassed one. Ignored by the pty backend * (which `exec`s the CLI directly, no shell wrapper, so it's trampoline-immune). */ launchShell?: string; /** * Optional model name passed to the CLI at spawn time (e.g. `claude --model * opus`). Each adapter decides how to inject it — adapters whose CLI has no * `--model` flag silently ignore the field. When unset, the CLI uses its own * default model. Multiple bots sharing the same `cliId` can therefore run * different models without resorting to wrapper scripts. See each adapter's * `modelChoices` for the curated candidates surfaced in `botmux setup`. */ model?: string; /** * Per-bot dsh runner turn timeout in milliseconds. The dsh adapter forwards * it as `--turn-timeout-ms` to the runner, overriding the built-in 10-minute * default (`DEFAULT_TURN_TIMEOUT_MS` in dsh-runner.ts). Positive integer * only; unset/≤0/non-integer → runner default. Only affects the `dsh` CLI * adapter; other adapters ignore the field. */ turnTimeoutMs?: number; /** * Per-bot dsh runtime variant. Only meaningful when `cliId === 'dsh'`: * - `'official'` (default): the headless JSON-RPC runner (dsh-runner.ts). * - `'tui'`: the interactive dsh-tui Ink TUI, driven via PTY (dsh-tui adapter). * Non-dsh CLIs always drop the field. Selected via the dashboard "dsh 运行时" * toggle; the worker resolves the effective adapter at spawn time. */ dshRuntime?: 'official' | 'tui'; /** Default Codex reasoning effort for newly created sessions. */ reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'ultra'; /** * If true, botmux does not add CLI-default approval/sandbox bypass flags * such as --yolo or --dangerously-*. Missing/false preserves legacy behavior. */ disableCliBypass?: boolean; /** Experimental Codex App input split. When true, newly accepted turns send * the real user text as app-server `input` and keep Botmux metadata in * `additionalContext`, so the desktop user bubble stays clean. Missing/false * preserves the legacy XML-ish prompt byte-for-byte. Codex App only. */ codexAppCleanInput?: boolean; /** * Per-turn 上下文注入方式(#794)。`auto`:对支持的 CLI(目前仅 claude-code), * 把 reminder/whiteboard 从 user turn 文本挪到 UserPromptSubmit hook 注入的 * system-reminder,终端输入框只保留消息本身;不支持的 CLI 自动回退内联。 * 缺省/`off`:保持内联 envelope(历史行为)。从下一个 follow-up turn 生效。 */ envelopeInjection?: 'auto' | 'off'; /** * Codex only (opt-in, experimental): deliver user input via the app-server * JSON-RPC channel instead of a tmux paste. The pane runs `codex --remote` * attached to a botmux-owned app-server thread, so input can't be dropped by * codex's terminal re-init. No effect on non-codex bots. */ codexRpcInput?: boolean; /** * Experimental local-only attachment mode for an already-running Codex App * Server. This does not make BotMux an app-server owner: after the operator * explicitly selects an existing thread via `/adopt`, BotMux launches the * official `codex --remote resume ` TUI as a second * client. For the `cliId: "codex"` shared-adopt path, new remote threads are * deliberately not auto-created; `cliId: "codex-app"` retains its existing * new-session workflow until the operator explicitly selects a shared thread. */ existingAppServer?: ExistingAppServerConfig; /** * Run this bot's CLI inside a per-session file sandbox (unified three-tier * whitelist, deny-by-default; Linux bwrap + macOS Seatbelt with identical * semantics — see adapters/cli/fs-policy.ts). The agent can read/write the * project + its own BOT_HOME, read the system toolchain baseline, and touch * NOTHING else. Env BOTMUX_SANDBOX=1 forces it on regardless (testing). */ sandbox?: boolean; /** * User增量 three-tier path lists layered ON TOP of the baseline preset * (never replacing it). Deepest matching rule wins, so nested black/white * lists work (readOnly a tree, deny a subdir inside it). Same semantics on * Linux and macOS. Absent → pure baseline. */ sandboxPaths?: { readWrite?: string[]; readOnly?: string[]; deny?: string[]; }; /** * LEGACY (pre fs-policy, kept for downgrade only): privacy masks under the * old read-everything model. Auto-migrated into sandboxPaths.deny at daemon * startup (old fields are kept on disk so a downgraded daemon still reads * them); no longer consulted by the new spawn path. */ sandboxHidePaths?: string[]; /** LEGACY: extra read-only paths — auto-migrated into sandboxPaths.readOnly * (see sandboxHidePaths note). */ sandboxReadonlyPaths?: string[]; /** * Whether the sandbox keeps network access. Missing/true preserves the existing * behavior; false adds bwrap --unshare-net for sessions that can run offline or * rely only on already-mounted local inputs. */ sandboxNetwork?: boolean; /** * LEGACY read-isolation flag (pre fs-policy). The unified sandbox is * deny-by-default, so cross-bot read isolation is inherent — this flag is * auto-migrated to `sandbox: true` at daemon startup and kept on disk only * for downgrade. No longer consulted by the new spawn path. */ readIsolation?: boolean; /** LEGACY: extra read-deny paths — auto-migrated into sandboxPaths.deny. */ readDenyExtraPaths?: string[]; backendType?: BackendType; /** * Configuration for the riff backend (agent-services platform). Required * when `backendType` is `'riff'`. Contains base URL, template ID, agent/model * selection, and auth settings for riff's HTTP API. */ riff?: RiffBackendConfig; /** * Configuration for the mojo backend (@byted/mojo headless CLI). Optional * even when `backendType` is `'mojo'`: every field has a working default * (`mojo` on PATH + an ambient login is a valid setup). Use it to pin the * model, inject a JWT, or force `--cloud` execution. */ mojo?: MojoConfig; /** * Max simultaneously-LIVE sessions for this bot. When the bot's live session * count exceeds this, the idle-worker sweeper suspends its longest-idle, * not-currently-busy sessions (resumable backends only) down to the cap — the * worker AND the CLI are killed to reclaim memory, and the session * cold-resumes from its on-disk transcript on the next message. Unset → the * built-in default {@link DEFAULT_MAX_LIVE_WORKERS} (30); an explicit positive * integer overrides it. Pure count-based: there is NO idle-time threshold. * Configured per bot from the dashboard (Groups & Bots → bot card). Adopted * sessions are never suspended. See core/idle-worker-sweeper.ts. */ maxLiveWorkers?: number; /** Periodically @ the persisted Session owner while selected actionable * runtime states remain unchanged. Missing means disabled. */ sessionOwnerReminder?: SessionOwnerReminderConfig; /** * When true, THIS bot's daemon watches host load/memory and DMs the bot owner * when the machine crosses into (and back out of) an overloaded state — a * heads-up that botmux session cold-starts may time out and false-die. Host * metrics are machine-wide, so designate ONE bot as the alerter; if several * have it on, a shared episode lock de-dups so the machine only DMs once per * edge. Missing/false = off. Hot-reloaded (no restart) once the daemon build * that ships the watcher is running. See core/host-overload-alert.ts and the * watcher in daemon.ts. BOTMUX_OVERLOAD_ALERT=0 force-disables regardless. */ overloadAlert?: boolean; /** Native Lark VC bot meeting copilot bridge. Push is primary; polling remains gate/backfill. */ vcMeetingAgent?: VcMeetingAgentConfig; workingDir?: string; workingDirs?: string[]; allowedUsers?: string[]; /** * Owner's native app-scoped `open_id` (`ou_…`), captured at setup from the * device-flow scanner identity. UNLIKE `allowedUsers` (which may hold `on_`/ * email entries needing a contact-API resolve every boot), this is stored raw * and never resolved — so it survives a contact-API outage. Two uses: * 1. a fail-safe DM recipient for allowedUsers-resolve failure notices, so * the owner is reachable even when the resolve that would have produced * their open_id is the very thing that failed (cold-start race); * 2. an always-available owner anchor for runtime permission checks. * Optional: bots created before this field, or via paths without a scanner * identity, simply have none and fall back to the resolved allowlist. */ ownerOpenId?: string; allowedChatGroups?: string[]; /** Oncall bindings: chat_id → default workingDir. Any group member can talk; allowedUsers still gates card buttons / daemon commands. */ oncallChats?: OncallChat[]; /** UI language for this bot: 'zh' or 'en'. Falls back to BOTMUX_LANG / LANG env when unset. */ lang?: Locale; /** How this bot's built-in botmux bridge skills reach its CLI (only meaningful * for CLIs with a global `skillsDir` — codex/gemini/opencode/…): * - `global`: install into the CLI's shared global skills dir (leaks into the * user's own standalone CLI). For users who never run the CLI by hand. * - `prompt`: inject a session-scoped skill catalog into the prompt + * `botmux skill show ` on demand. No leak. * - `off`: routing hints + `botmux --help` only. * Unset ⇒ fall back to the machine-wide `skills.builtinInjection` (default * `prompt`). See services skills/injection-mode.ts. */ skillInjection?: 'global' | 'prompt' | 'off'; /** * Per-bot default working directory. When set, new topics that have no * oncall binding and no sibling-session inheritance skip the repo-select * card and spawn the CLI directly in this directory. `/cd ` still * works to switch mid-session; the next new topic falls back to this default. * * Pure runtime fallback — does NOT write any state to bots.json and does * NOT change the canTalk / canOperate permission model (unlike defaultOncall). */ defaultWorkingDir?: string; /** * 「仅默认目录」模式下的开关:新会话启动前,先在 `defaultWorkingDir`(须是 git 仓库) * 基于远端默认分支自动创建一个 linked worktree,再把会话 cwd 指向该 worktree,实现 * 每个新会话一个隔离 checkout。仅在 mode==='default'(defaultWorkingDir 有值)时有意义; * 非 git 仓库 / 创建失败时回退直接用 defaultWorkingDir 启动。复用 `/repo wt` 的 * createRepoWorktree。见 services/default-worktree.ts。 */ defaultWorkingDirAutoWorktree?: boolean; /** Per-bot default: auto-bind every new group chat to oncall on first new-topic. */ defaultOncall?: BotDefaultOncall; /** * Chat IDs that have ever been auto-bound by `defaultOncall`. Append-only. * Once a chat appears here, the default is permanently "spent" for it — even * if the user later unbinds via Groups & Bots / `/oncall unbind`, the * default will not re-bind it. This preserves the manual-override semantics * Codex flagged in review. */ defaultOncallAutoboundChats?: string[]; /** Per-chat reply mode: chat_id → 普通群 @bot 后回复形态。缺省为 chat(保持现状)。 */ chatReplyModes?: { [chatId: string]: ChatReplyMode; }; /** Per-chat per-user grants: chat_id → 被授权的 open_id 列表。仅放行 canTalk,不给管理命令权。 */ chatGrants?: { [chatId: string]: string[]; }; /** * 全局对话授权名单:被授权在**任意群**与本 bot 对话的 open_id 列表(人或 bot 通用)。 * 与 chatGrants 同属 talk-only —— 仅放行 canTalk / bot 路由闸,**canOperate 绝不读它** * (敏感操作仍仅限 allowedUsers)。这是 chatGrants 的全局版:作用域升到全局,talk-only * 性质不变。可由 /grant 卡片「全局」按钮写入,也可在 bots.json 手配 open_id。 */ globalGrants?: string[]; /** Additional plugin ids enabled only for this bot. */ plugins?: string[]; /** * 私聊对话全开(默认关闭)。开启后**任何人都能和本 bot 私聊**(talk-only),无需 * 逐个加 globalGrants —— 谁能私聊由飞书应用的「可用范围」控制,botmux 侧不再设闸。 * * **只放行 canTalk,canOperate 绝不读它**:`/restart`、`/cd`、`/repo`、卡片按钮等 * 管理操作仍只认 allowedUsers。与 oncall(群维度的 talk-open,管理权仍限 owner) * 是同一个安全模型,本字段只是把它补到 p2p 维度——oncall/defaultOncall 明确不绑 * p2p(oncall-store.ts 的 `chatType !== 'group'` 短路),故私聊此前只有「逐人白名单」 * 与「三张名单全空 → 人人是 admin」两个极端。 * * 不影响群:群里仍按 allowedUsers / allowedChatGroups / oncall / grants 判定。 */ p2pOpen?: boolean; /** * 是否接受**其他 bot** 通过 `botmux send --slash` 发来的原生斜杠命令 * (/clear、/model、/close…)。默认开(undefined = 开);只有显式 false 才关。 * * 关掉后,来自 bot 发送方的 slash 命令不进 passthrough / daemon-command 路由, * 退化为普通消息(与任何非 bot-slash 消息一样按 talk 门处理)——给 owner 一个 * 「不让别的 bot 清我上下文 / 敲我 CLI」的逃生阀。对**真人**发送方无影响 * (真人在飞书直接打字发 /clear 仍照常)。 * * 安全边界不变:daemon 管理命令(/close /restart 等)从 bot 来**仍只认 * allowedUsers**(canOperate),本开关只控制「是否接受 bot 的 slash 进入路由」, * 不放宽任何 operate 权限。 */ acceptSlashFromBots?: boolean; /** * 消息额度覆盖配置: * • 未配置(undefined)→ 卡片使用产品默认 3 条;oncall 不自动计数。 * • 配置正整数 D → 卡片默认 D 条,同时作为 oncall 默认额度。 * 显式 `/grant @x N` 的 N **恒生效**,与本字段是否配置无关(见 {@link quotaState})。 * 仅约束 chatGrants / globalGrants 这类 per-user talk 授权,绝不影响 canOperate。 */ messageQuota?: { defaultLimit?: number; }; /** * 新建 per-user 授权卡的默认有限时长(毫秒)。缺省使用产品默认 1 小时; * 已存在授权和已经生成的 pending 卡不受后续配置变更影响。 */ grantDefaultDurationMs?: number; /** * scope-aware 消息额度计数(运行时状态,随授权一起持久化进 bots.json)。 * key = `chat:${chatId}:${openId}` | `global:${openId}`,value = { limit, used }。 * 仅在 /grant 带额度(显式数字,或开启 default 时取 default)时建记录; * used 达到 limit 后自动收回**对应 scope** 的授权并删除本记录。纯 talk-only。 */ quotaState?: { [quotaKey: string]: { limit: number; used: number; }; }; /** * scope-aware 授权绝对过期时间。缺少对应记录表示永久授权;旧配置因此保持兼容。 * key 与 quotaState 相同,便于授权、撤销和到期回收在同一 scope 上原子处理。 */ grantExpiryState?: { [grantKey: string]: { expiresAt: number; }; }; /** * 开启后:仅靠 per-user 授权(chatGrants / globalGrants)放行的发送者,禁止使用**任何 * 斜杠命令**——botmux 自身的 DAEMON 命令、透传(PASSTHROUGH)命令、全部 `/workflow` * 子命令、已退休的 `/template` tombstone、`/introduce`、`/t`/`/topic` —— 只能普通对话。owner / allowedUsers / oncall / * allowedChatGroup 整群成员不受影响。判定以 slash-command invocation 命中为准(不是"凡以 * `/` 开头的文本",避免误伤讨论命令用法的普通对话)。默认 false(保持现状:被授权人可用透传)。 */ restrictGrantCommands?: boolean; /** * 自动授权申请卡开关。默认开启(undefined = on):群里有人或外部 bot 明确 @ 本 bot * 但被 talk 权限闸挡住时,给 owner 弹 /grant 申请卡。显式 false 时静默丢弃, * 保留原来的强权限闸但不刷卡。 */ autoGrantRequestCards?: boolean; /** * 用户自定义、额外放行透传给 CLI 的 slash 命令 —— 在固定的 PASSTHROUGH_COMMANDS * 之上扩展(例如把 CLI 支持但默认不放行的 `/goal`、`/export` 加进来)。每项必须 * `/` 开头、小写、仅含 [a-z0-9:_-];解析时归一化(缺失的 `/` 自动补、转小写、去重、 * 丢弃非法项与会遮蔽 botmux daemon 命令的项)。与内置白名单合并后由 * {@link resolvePassthroughCommands} 生效;`/list-slash-command` 可查看完整放行清单。 * 未配置(undefined)→ 仅用内置白名单(保持现状)。 */ customPassthroughCommands?: string[]; /** * Daemon 命令的权限例外名单:列出的命令把权限闸从 canOperate(仅 allowedUsers)降到 * canTalk(oncall 群成员 / allowedChatGroups / chatGrant / globalGrant / p2pOpen 私聊 * 等对话放行腿)。与 passthrough 无关——命令仍由 daemon 自己处理,只是准入门槛不同。 * 解析时归一化(转小写、自动补 `/`、去重),且**只接受 DAEMON_COMMANDS 内的命令**, * 其余条目丢弃并 warn。带 handler 内部第二道 owner 闸的命令(/card /term /insight) * 即使列入也仍会被内部闸拒绝(fail-closed,不视为本字段的适用对象)。 * ⚠️ 与 `restrictGrantCommands` 的组合:那个开关在路由里先于本名单生效——开着时 * chatGrant/globalGrant 被授权人发任何 slash 命令都被更早的限制闸挡下,本名单 * 对他们不生效(oncall / allowedChatGroups / p2pOpen 等其余 canTalk 腿不受影响)。 * 未配置(undefined)→ 全部 daemon 命令保持 owner-only(现状)。 */ canTalkDaemonCommands?: string[]; /** * Optional per-bot startup commands: slash-command lines the worker types into * a freshly spawned CLI right after it's ready, BEFORE the user's first prompt * (e.g. `/effort ultracode`, `/model opus`). Sent in order, one submit each, * via the same literal-input path as a passthrough slash command (no prompt * wrapping). Re-applied on every fresh spawn (incl. resume) — so session-only * settings like `/effort ultracode` survive a resume. Skipped in adopt mode * (we observe the user's existing session, not drive a fresh one). Each entry * is trimmed and gets a leading `/` if missing; arguments (spaces) preserved. */ startupCommands?: string[]; /** * Optional per-bot environment variables, injected into THIS bot's CLI * process (e.g. `ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN` to run the bot * on GLM / a third-party Anthropic-compatible provider, an `HTTPS_PROXY`, or * a CLI feature flag). Sanitized at load via {@link sanitizePerBotEnv} * (valid env-var names + string/number/boolean values; botmux-reserved keys * dropped). Delivered per-session as SpawnOpts.injectEnv so it never pollutes * the shared tmux/zellij server env. Missing/empty → undefined. */ env?: Record; /** * Optional per-bot priority skill policy. Missing means botmux does not alter * the underlying CLI's native skill discovery or spawn arguments. */ skills?: BotSkillPolicy; /** * Custom footer brand label for cards this bot sends. Three states: * • `undefined` (unset) → default `[botmux](github)` link * • `''` (empty) → brand suppressed (footer shows only 发送给 if any) * • any other string → rendered verbatim (markdown allowed) * Resolved via {@link resolveBrandLabel}. Pure cosmetic — does not affect * routing or permissions. */ brandLabel?: string; /** * Where to show native Context / Token usage for this bot's Session cards: * • `'streaming'` (default / unset) → in the live streaming card body * • `'footer'` → in the ordinary reply-card footer * • `'off'` → nowhere * A missing individual metric is still omitted independently, and this only * controls DISPLAY — Usage Ledger accounting and other consumers are * unaffected. Backward compat: a legacy `showUsageInCardFooter: false` with no * `usageDisplay` set is read as `'off'` (see {@link resolveUsageDisplay}). */ usageDisplay?: UsageDisplayMode; tuiSlashAllow?: string[]; /** * When true, suppress the live streaming session card entirely. The web * terminal still runs and the final answer still arrives via `botmux send`; * only the auto-updating status card is never posted/patched. Default * (undefined) keeps the streaming card. For users who find the live card noisy. */ disableStreamingCard?: boolean; /** * When true, suppress the lightweight GoGoGo → DONE message reactions used as * progress markers in card-off sessions. Missing/false preserves the current * card-off reaction behavior. */ silentTurnReactions?: boolean; /** * Feishu emoji_type for the "received" turn reaction in card-off sessions. * Undefined → default GoGoGo (冲!). Free-form string; a bad emoji_type just * silently fails to attach (addReaction is best-effort). */ receivedReactionEmoji?: string; /** * Feishu emoji_type for the "done" turn reaction. Undefined → default DONE (✅). * Set this EQUAL to receivedReactionEmoji to keep the marker visually * unchanged on turn-end — useful for CLIs whose idle detection can fire early * (e.g. Pi during model-thinking gaps), where a premature ✅ would mislead. */ doneReactionEmoji?: string; /** * Conversation mode for 1:1 private chats (DMs) with the bot: * - 'thread' (default, stored as undefined): every top-level DM message * starts a fresh thread-scoped session — the official/legacy behavior, * keeps 1:1 chatter out of one long-running CLI process. * - 'chat': route DMs as one flat, continuous chat-scoped session (all * messages share the same context, similar to Hermes/OpenClaw). * - 'group': every top-level DM message births a dedicated 1-user+1-bot * "session group" that hosts the conversation (the bot keeps chat * ownership; the group is registered in session-groups-store and the * session lands chat-scope inside it). Falls back to 'thread' behavior * when group creation fails. * Editable at runtime via `/botconfig p2pMode chat|thread|group` (owner/admin). */ p2pMode?: 'thread' | 'chat' | 'group'; /** * Settings for p2pMode='group' session groups. All fields optional; see * SessionGroupConfig for defaults. Ignored under other p2pModes. */ sessionGroup?: SessionGroupConfig; /** chat_id list: chats where the live streaming card is suppressed (status falls back to master's pending-card morph). Written by `/card off|on`. */ noCardChats?: string[]; /** * When true, the streaming card embeds a directly-usable WRITABLE terminal * link in its body (token included → anyone who can see the card can drive * the terminal). Default (undefined) keeps the write link behind the * "get write link" button, which DMs it privately to the clicker. Moot when * {@link disableStreamingCard} is on (no card to embed it in). */ writableTerminalLinkInCard?: boolean; /** * When true, `/card` sends a **private** static snapshot card via the ephemeral * API, visible only to the bot's `allowedUsers` (owner / co-owners), instead of * the group-visible live streaming card. Talk-only grants (globalGrants / * chatGrants) and a bare triggerer do NOT receive it — it's owner-only. Only * works in plain `group` chats (topic/thread/p2p fail closed) and cannot * live-update (ephemeral cards can't be patched). Scoped to the `/card` command * only — the auto streaming card is unaffected. Default (undefined) keeps * `/card` group-visible & live. */ privateCard?: boolean; /** * bot@bot 同目录拉起 (cross-bot working-dir inheritance). When a bot is @-ed * into a chat/thread where a sibling bot already has an active session, it * reuses that sibling's workingDir and skips its own repo-selection card. * This is independent of /oncall. Default ON (undefined = on); set to false * to make THIS bot always fall through to its own repo card / default dir. * Toggled from the dashboard Bot Defaults tab; persisted via card-prefs-store. */ botToBotSameDir?: boolean; /** * 平台团队页是否展示这个 bot. When false, this bot is hidden from the central * platform's team roster (人→机器→bot view). Default ON (undefined = shown); * set to false to keep an internal/utility bot off the team page. * Reported to the platform via the dashboard's bot-info upload. */ showInTeam?: boolean; /** * 主动开工 — 场景①. When true, the bot auto-starts a session when it is added * to a new chat that contains at least one of its allowedUsers (see * docs/specs/20260529-proactive-auto-start/). Default (undefined) = passive * (only spawns on @mention). Requires the `im.chat.member.bot.added_v1` event * to be subscribed for the app in the Feishu console. */ autoStartOnGroupJoin?: boolean; /** * 主动开工 — 场景① optional pre-configured first-turn prompt. When set, it * becomes the user_message of the auto-started session; when unset/blank the * session starts with an empty user_message and the bot reads the group * context itself. Moot when {@link autoStartOnGroupJoin} is off. */ autoStartOnGroupJoinPrompt?: string; /** * 进群自动拉 owner。Default (undefined) = ON:本 bot 被加进任何群时,自动把 * 自己的 owner(resolvedAllowedUsers 首个 ou_ 用户)拉进群——bot 应始终处于 * owner 可见的群里(不打黑工)。显式 false 关闭(如告警/oncall 类 bot 被 * 平台批量拉进大量事件群、不想打扰 owner 的场景)。仅 bots.json 文件配置。 */ autoInviteOwnerOnGroupAdd?: boolean; /** * 主动开工 — 场景②. When true, in a 话题群 (topic mode) every new topic's first * message auto-starts a session even without an @mention (the default role + * the user's first message form the prompt). No effect in regular groups. * Default (undefined) = passive. */ autoStartOnNewTopic?: boolean; /** * Per-chat group message listener. Keyed by chat_id and bot-scoped so the * dashboard can configure it from the Roles page's natural group × bot * matrix. When enabled, the bot may react to non-@ top-level group messages * after deterministic sender/msgType filtering. V1 always replies in a * fresh thread under the triggering message. */ messageListeners?: Record; /** * Worktree picker mode on the repo-select card. When true, the worktree * control renders the multi-repo selector (pick N repos + branch) instead of * the single-select dropdown. Toggled from the card's 「切换多仓库选择器」button; * persists so all of this bot's future sessions default to it. Default false. */ worktreeMultiPicker?: boolean; /** * Per-bot DEFAULT session mode for regular Lark groups (overridable per-chat * via `/reply-mode` → `chatReplyModes`). Resolved by * `chat-reply-mode-store.regularGroupDefaultMode`. * • 'chat' (or undefined) — whole group shares one flat chat-scope session * • 'new-topic' — each top-level @mention forks its own thread-scope session * • 'shared' — replies fold into a topic but reuse the one chat-scope session */ regularGroupReplyMode?: ChatReplyMode; /** * Per-bot (bot-global) policy for when an @mention is required to get a reply * in regular Lark groups — a 4-tier ladder: * • 'always' (or undefined) — @ required everywhere, including inside the * bot's own shared topics (the safe default). * • 'topic' — @ required to start / at top level, but NOT * inside the bot's shared topics (non-@ replies * there continue the session). * • 'never' — @ never required: every non-@ message in groups * where the bot has talk access is answered too, * unconditionally. For dedicated / on-call groups. * • 'ambient' — like 'never' (non-@ messages answered), EXCEPT * when the message @mentions another specific * member (person/bot) without @ing this bot — * that is a redirect to someone else, so the bot * stays quiet (@all is not a redirect). Best for * multi-bot / multi-person groups: a default * responder that yields when you address someone * else. * Governs the shared-topic fold-back + the top-level @ gate. `new-topic` / * 话题群 topics own their own thread and continue without @ regardless (that * is the mode's defining behavior, not affected by this policy). */ regularGroupMentionMode?: 'always' | 'topic' | 'never' | 'ambient'; /** * Regular-group substitute trigger. When enabled, an @mention of one of the * configured people is treated as an address to this bot when the sender can * talk to the bot. Matching currently uses mention open_id / user_id / union_id; * email is preserved for future resolution but is not matched directly. */ substituteMode?: SubstituteModeConfig; /** * 飞书文档评论监听(/watch-comment;/subscribe-lark-doc 也复用)新绑定的默认触发范围: * • 'mention-only'(或 undefined)— 仅评论里 @bot 才触发(默认,防噪声) * • 'all' — 该文档所有新评论都触发 * 单条订阅的触发范围之后可在 dashboard 逐文档改(doc-subscriptions 表)。 */ docSubscribeDefaultMode?: 'mention-only' | 'all'; /** * 文档 → 本地仓库/目录映射。当文档评论触发且无活跃 session 时,auto-create * session 会按 fileToken 查此表确定 agent 的 workingDir。 * 键是飞书文档的 file_token(wiki 已解析为底层 obj_token),值是本地绝对路径。 * 例:{ "KszRdLt6MoNtBFxNjBmm3jlhyWd": "/home/me/my-repo" } * 也可以在 `/watch-comment --dir /path` 时逐文档指定。 */ docRepoMap?: Record; /** Per-bot range for explicit `@bot /summary`; defaults to 50 messages / 24h. */ summaryRange?: SummaryRangeConfig; /** When true, explicit `@bot /summary` records a conservative project-local summary.md. */ summaryMemory?: boolean; /** Optional target path for summary memory. Relative paths are resolved by the agent against the current project root; absolute paths are used as configured. */ summaryMemoryPath?: string; /** * Legacy content/keyword trigger config. Kept parseable for config * compatibility, but message routing no longer fires non-@ content triggers. */ contentTriggers?: ContentTriggerConfig[]; /** * Per-bot voice-engine override for the voice-summary feature. Merged OVER * the global `voice` block in ~/.botmux/config.json (per-bot wins field by * field). When this bot has usable voice creds (here or globally), its reply * cards render the "🔊 语音总结" button. See services/voice/types.ts. */ voice?: VoiceConfig; } export interface BotState { config: BotConfig; /** The Lark SDK client — NULL for apiOnly (core-only) bots: they have no * Feishu credential (empty appSecret), and the SDK's Client ctor throws * "appSecret or clientAssertionProvider is required" on an empty secret. An * apiOnly bot never needs it (getBotClient throws LarkTransportDisabledError * before returning it), so we skip construction entirely rather than feed the * SDK a placeholder. Every consumer reaches it via getBotClient (which gates * apiOnly) or getAllBotClients (which filters apiOnly), so the null is unreachable. */ client: Lark.Client | null; /** Same credentials/domain as `client`, but bound to a dedicated http * instance with the looser upload timeout. Only media uploads use it. NULL for * apiOnly bots for the same reason as `client` (no credential to construct one); * getBotUploadClient gates apiOnly before returning it, so the null is unreachable. */ uploadClient: Lark.Client | null; botOpenId?: string; botName?: string; botAvatarUrl?: string; resolvedAllowedUsers: string[]; /** raw allowedUsers 条目 → 解析后的 open_id。供 /revoke 反查并删除 email 形式的 raw 条目。 */ rawAllowedUserResolution: Map; } export declare function __testOnly_resetBotRegistry(): void; export declare function getLoadedConfigPath(): string | undefined; /** * Provenance of `getLoadedConfigPath()`. `undefined` when nothing has been * resolved yet. Consumed by the worker to decide whether the path is a real * registry authority worth pinning onto a CLI child's `BOTS_CONFIG`. */ export declare function getLoadedConfigProvenance(): BotsConfigProvenance | undefined; /** * Condense a Lark SDK error into one readable line, preserving just the fields * needed to triage (HTTP status + business `code`/`msg`/`log_id`). Returns null * when the value isn't an axios-shaped error, so callers fall back to * length-capped stringify. Never serializes `config`/`headers`/`stack`, so the * access token can't leak. */ export declare function formatLarkError(v: any): string | null; /** * Pure predicate: is this bot's VC-meeting-agent config ACTIVE (should the daemon * attend meetings / restore VC runtime sessions / poll `lark-cli vc` for it)? * * Returns the config only when it is `enabled` AND the bot is NOT apiOnly. An * apiOnly (core-only) bot has no Feishu connection — attending a VC meeting drives * `lark-cli vc +meeting-events --as bot`, which categorically violates the * zero-Feishu-network contract. Gating here (rather than only at each call site) * means the daemon's central `effectiveVcMeetingAgentConfig` accessor — and every * one of its ~24 consumers, including the boot-time `restoreVcMeetingRuntimeSessions` * path that runs OUTSIDE the `!cfg.apiOnly` boot block — fail-closes for apiOnly by * construction. The dashboard already refuses to SET an apiOnly listener; this also * covers a hand-edited / migrated bots.json (a normal VC bot flipped to apiOnly with * `vcMeetingAgent.enabled:true` + a stale on-disk runtime record). */ export declare function vcMeetingAgentConfigActive(cfg: Pick | undefined): VcMeetingAgentConfig | undefined; export declare function registerBot(cfg: BotConfig): BotState; export declare function getBot(larkAppId: string): BotState; export declare function getBotClient(larkAppId: string): Lark.Client; /** Client bound to the looser upload timeout. Use only for media uploads * (image/file); every other call uses `getBotClient` and its interactive bound. */ export declare function getBotUploadClient(larkAppId: string): Lark.Client; /** Owner = bot 首个已授权 open_id,与「缺权限警告私信对象」同口径(见 admin 解析)。 */ export declare function getOwnerOpenId(larkAppId: string): string | undefined; /** Admins = all resolved allowedUsers, matching `/botconfig`'s permission model. */ export declare function getDashboardAdminOpenIds(larkAppId: string): string[]; export declare function setResolvedAllowedUsersRepublishHook(fn: (larkAppId: string, resolved: string[]) => void): void; export declare function republishResolvedAllowedUsersDescriptor(larkAppId: string, resolved: string[]): void; export declare function setAllowedUsersResolveRetryHook(fn: (larkAppId: string) => void): void; export declare function scheduleAllowedUsersResolveRetryFromMutation(larkAppId: string): void; /** Bot 自身的 open_id(用于在 mention 解析时排除自己)。 */ export declare function getBotOpenId(larkAppId: string): string | undefined; /** * 安全地按 appId 取 brand。未注册(如跨进程 dashboard 聚合到别的 daemon 的 * 会话)→ 归一为 'feishu'。仅用于派生 applink 等 host,缺省 feishu 安全。 */ export declare function getBotBrand(larkAppId: string | undefined): Brand; export declare function getAllBots(): BotState[]; /** * Bot 的有效展示名:自定义 displayName > 飞书探测名 botName > larkAppId。 * 仅用于展示面(dashboard descriptor / SessionRow);@ 路由与 bots-info * 名册仍用飞书真名。 */ export declare function effectiveBotDisplayName(state: BotState): string; /** Lookup the oncall binding for a given bot+chat, if any. */ export declare function findOncallChat(larkAppId: string, chatId: string): OncallChat | undefined; /** * The bot's effective default working dir for a NEW session, as a raw * (possibly `~`-prefixed) path — the caller still expands + validates it. * * Two sources, presented as a mutually-exclusive 3-way choice in the dashboard * ("默认工作目录模式": 关闭 / 仅默认目录 / Oncall 模式) but if both happen to be set * (legacy / chat-command config) `defaultWorkingDir` wins: * 1) `defaultWorkingDir` — pin a dir for new sessions; no permission change. * 2) `defaultOncall.workingDir` when `defaultOncall.enabled` — "Oncall 模式" * extends its directory to ALL of this bot's sessions (p2p / 话题 / 普通群 * fallback), not just the group auto-bind. The group auto-bind (which also * opens talk to the whole group) still happens separately upstream; this * fallback is what makes the bot's OTHER sessions land in the same dir. * * Returns undefined when neither is configured. Reading this NEVER writes state * or binds a chat to oncall, so the resolved session's permission model is * unchanged regardless of which source supplied the path. */ export declare function effectiveDefaultWorkingDir(cfg: BotConfig): string | undefined; export declare function findOncallChatForAnyBot(chatId: string): OncallChat | undefined; export declare function isChatOncallBoundForAnyBot(chatId: string): boolean; /** Normalize a raw bots.json entry's usage-display intent to the enum, applying * backward compat: an explicit `usageDisplay` wins; otherwise a legacy * `showUsageInCardFooter: false` maps to `'off'`; everything else is the * default (`'streaming'`). Single source of truth for both the in-memory parse * and the disk-fallback resolver so they cannot drift. */ export declare function normalizeUsageDisplay(entry: { usageDisplay?: unknown; showUsageInCardFooter?: unknown; }): UsageDisplayMode; /** * The configured brand label for a bot, or `undefined` when unset (`''` = off * is preserved). Prefers the in-memory registry (daemon hot path); falls back * to a mtime-cached read of bots.json so the CLI process — which never loads * the registry — still resolves the sending bot's brand. Callers feed the * result into {@link brandFooterSegment} for the unset→default / ''→off rule. */ export declare function resolveBrandLabel(larkAppId: string): string | undefined; /** * Resolve the per-bot usage-display mode (default `'streaming'`). A freshly * loaded registry wins over the spawn-time env so long-lived panes observe * `/botconfig` hot updates; sandboxed/env-only processes carry the value in * their synthetic registered bot and otherwise fall back to the injected env. */ export declare function resolveUsageDisplay(larkAppId: string): UsageDisplayMode; /** * 只读 accessor:该 bot 配置的 tuiSlashAllow allowlist(TUI 通用 slash 注入用)。 * 仅读内存态注册表,daemon 进程内使用;无需 bots.json 磁盘回退(不同于 * resolveBrandLabel——`botmux send` 等一次性 CLI 进程不消费此 accessor)。 */ export declare function getBotTuiSlashAllow(larkAppId: string): string[] | undefined; /** * 该 bot 是否接受**其他 bot** 发来的原生斜杠命令(--slash)。默认开:只有 * 配置里显式 `acceptSlashFromBots: false` 才关。未知 bot(无注册项)→ 默认开 * (与其它 default-on 开关一致,缺配置不 fail-closed 成"全拒")。 */ export declare function botAcceptsSlashFromBots(larkAppId: string): boolean; /** * Load bot configurations from one of (in priority order): * 1. BOTS_CONFIG env var — path to a JSON file * 2. ~/.botmux/bots.json — default config path * 3. Core-only (BOTMUX_CORE_ONLY=1) with NEITHER of the above present: * synthesize a single apiOnly bot from env — no bots.json / no Feishu creds * (riff's in-sandbox headless service). */ export declare function loadBotConfigs(): BotConfig[]; /** * Resolve one daemon's exact raw bots.json slot without compacting earlier * activation-pending entries. PM2 assigns BOTMUX_BOT_INDEX from the durable * array index; filtering the array first would make a later ready bot load a * different App whenever concurrent onboarding left an earlier slot pending. */ export declare function loadBotConfigAtIndex(index: number): BotConfig; /** * Direct managed activation daemons are allowed to boot only while their * exact raw config row carries a matching startup marker. They wait before * registering until the dashboard records the exact PM2 identity ACK. */ export declare function isManagedActivationStartingAtIndex(index: number, appId: string, jobId: string): boolean; /** Pure parser: bots.json text → BotConfig[]. Exported for testing & reuse. */ export declare function parseBotConfigsFromText(jsonText: string): BotConfig[]; export declare function readBotSkillPolicy(raw: unknown): BotSkillPolicy | undefined; //# sourceMappingURL=bot-registry.d.ts.map