import type { CliId, ResumableSession } from '../adapters/cli/types.js'; import { type WorkerSessionReplyOptions } from './worker-pool.js'; import { validateWorkingDir } from './working-dir.js'; import { type AdoptableSession } from './session-discovery.js'; import { type ZellijAdoptableSession } from './zellij-adopt-discovery.js'; import { type CodexAppThreadSummary } from '../services/codex-app-threads.js'; import { type DocSubscription } from '../services/doc-subs-store.js'; import type { LarkMessage, FrozenSessionReplyTarget } from '../types.js'; import type { DaemonSession } from './types.js'; import { DAEMON_COMMANDS, PASSTHROUGH_COMMANDS } from './passthrough-commands.js'; export { DAEMON_COMMANDS, PASSTHROUGH_COMMANDS }; /** * Daemon commands that act on the chat itself rather than opening a * conversation. `/group` (`/g`) just creates a Lark group and replies once — * no follow-up turns, no CLI worker. The new-topic spawn path normally * pre-creates a sessionStore record so a command can attach state and keep * card buttons routable, but for these that record is a phantom conversation * that pollutes the dashboard's session list. Handle them without a session. */ export declare const SESSIONLESS_DAEMON_COMMANDS: Set; /** Apply the machine-wide prefix used only by `/group` and `/g`, then keep the * existing Lark headroom. The legacy limit is measured in UTF-16 code units; * iterating by code point keeps that limit without slicing an emoji's * surrogate pair. */ export declare function formatSlashGroupName(name: string, prefix?: string): string; /** * Daemon commands that operate on an ALREADY-EXISTING session and must never * pre-create one. With no real session to operate on, the daemon routes must skip their generic * "createSession + activeSessions.set(worker:null)" pre-create block and let * handleCommand's `!ds` branch reply no_active_session. Without this, one of these commands * in a brand-new topic (or a thread with no session) would spawn a phantom * worker:null session just to handle it, polluting the dashboard. (Same class * of fix as the `/card` / `/term` special cases in daemon.ts.) */ export declare const EXISTING_SESSION_ONLY_DAEMON_COMMANDS: Set; /** * Adapter-scoped default passthrough commands (e.g. Codex's `/goal`). * * `cliIdOverride` lets a caller resolve against a session's FROZEN CLI instead * of the bot's current config — an existing session keeps the runtime it was * created with, so changing `/botconfig cli` must not silently strip an old * interactive Codex session's adapter-scoped `/goal` (nor grant one to a Codex * App session). `defaultPassthroughCommands` is a static per-adapter list and * does not depend on the resolved binary, so when the override diverges from * the bot's current CLI we intentionally drop `cliPathOverride` (it belongs to * the other CLI) and let the adapter resolve with no path hint. */ export declare function resolveAdapterDefaultPassthroughCommands(larkAppId?: string, cliIdOverride?: string): string[]; export declare function cliHasNoRawPassthroughSurface(cliId: string | undefined): boolean; export declare function resolvePassthroughCommands(larkAppId?: string, cliIdOverride?: string): Set; export interface SlashCommandInvocation { cmd: string; content: string; } export { validateWorkingDir }; /** * Resolve a non-numeric `/repo ` into a concrete repo path + display name. * `arg` is either a path (absolute or relative) or a first-level project name * under one of the bot's scan dirs — letting the user skip the selection card. * * Resolution: * 1. Build candidate absolute paths — absolute / `~` taken as-is; relative or * bare names resolved against each scan dir, then the daemon cwd (mirrors * how the card's project list is rooted). * 2. Return the first directly existing candidate, describing its git ref * without scanning unrelated roots. This is lenient like `/cd`, whose trust * model is "owner explicitly chose a dir"; the CLI already runs with full * FS access. * 3. Only for a bare name that did not directly resolve, scan projects and * match by basename (covers projects nested deeper than the scan-dir top * level). * Returns null when nothing resolves to an existing directory. */ export declare function resolveRepoSelection(repoArg: string, scanDirs: string[]): { path: string; displayName: string; } | null; /** * Parse a force-topic invocation: `/t [prompt]` or `/topic [prompt]`. * * This is a routing meta-command, distinct from `parseSlashCommandInvocation` * (which routes to daemon command handlers). The match conditions are * deliberately tighter than the regular slash parser: * * - exact-prefix match (`/t` / `/topic`, case-insensitive); `/tea` / `/topical` * must NOT match, otherwise we'd false-trigger on common /-prefixed words. * - tolerates leading whitespace (mention-stripping can leave a space). * - prompt is whatever follows the prefix (verbatim, including newlines). * - `/t` alone (no args) is allowed → empty prompt; the daemon treats it as * topic setup, choosing either a repository picker or a visible thread that * waits for the first real task according to the bot's cwd configuration. * * Returns null for anything else, so callers can fall through to the regular * `parseSlashCommandInvocation` / message-handling path. */ export declare function parseForceTopicInvocation(content: string): { prompt: string; } | null; /** Parse a user-authored slash command after leading @mentions have already * been stripped. Messages that look like command examples or command lists * are intentionally left for the CLI instead of being intercepted by the * daemon; otherwise discussion text such as `/adopt ` can accidentally * trigger real daemon actions. */ export declare function parseSlashCommandInvocation(content: string): SlashCommandInvocation | null; export interface CommandHandlerDeps { activeSessions: Map; sessionReply: (rootId: string, content: string, msgType?: string, larkAppId?: string, turnId?: string, opts?: WorkerSessionReplyOptions) => Promise; getActiveCount: () => number; lastRepoScan: Map; /** Immutable Lark placement captured by the daemon for this slash-command * invocation. Unlike session state, it remains valid after close/replace. */ invocationReplyTarget?: FrozenSessionReplyTarget; /** 会前预热文档评论会话:立即启动 CLI、读取文档并进入待命。 */ prewarmDocCommentSession?: (ds: DaemonSession, sub: DocSubscription) => Promise; } /** * Handle `/card` (operator-only). Resolves the active session itself, so off/on * work WITHOUT one -- they only toggle the per-chat `noCardChats` config. A * summon (show/bare) needs a live session. * * off -> suppress the live streaming card for this chat (add to noCardChats); * status falls back to master's pending-card morph. * on -> restore cards for this chat (remove from noCardChats). * ''/show -> summon a live card. privateCard -> private ephemeral snapshot * (fail closed on non-group); otherwise a group-visible live card. * off/on also clear `streamingCardForced` so a prior summon does not * short-circuit `streamingCardDisabled()`. */ export declare function handleCardCommand(rootId: string, larkAppId: string, chatId: string, senderOpenId: string | undefined, content: string, deps: CommandHandlerDeps): Promise; /** * Handle `/term` (operator-only) — the slash-command twin of the "🔑 获取操作链接" * card button. Privately hands the operator a writable (token-bearing) terminal * card: an in-chat visible-to-you ephemeral card in plain groups, auto-falling back * to a DM in topic / p2p chats. The link rides only that private channel — never the * group. Gated identically to /card (`canOperate`), and strictly needs a live * session whose terminal is up. Routed for both the new-topic path (daemon.ts) and * the existing-session switch below. */ export declare function handleTermLinkCommand(rootId: string, larkAppId: string, chatId: string, senderOpenId: string | undefined, _content: string, deps: CommandHandlerDeps): Promise; export declare function handleCommand(cmd: string, rootId: string, message: LarkMessage, deps: CommandHandlerDeps, larkAppId?: string): Promise; export declare function startCodexAppThreadSession(thread: CodexAppThreadSummary, ds: DaemonSession, deps: CommandHandlerDeps, larkAppId?: string): Promise; export declare function startAdoptSession(target: AdoptableSession | ZellijAdoptableSession, ds: DaemonSession, deps: CommandHandlerDeps, larkAppId?: string): Promise; /** Cap on resume candidates surfaced by the /adopt picker. Kept at the legacy * 20 (per product call: the V2 card is a display change, not a scope change). * When the cap is hit the card shows a hint pointing at search + the * `/adopt ` direct path, so history beyond the cap is still reachable. */ export declare const ADOPT_RESUME_LIMIT = 20; /** Discover the sessions resumable from disk for `cliId`, excluding any whose * CLI-native id is already live in a botmux session (so a session botmux * already runs isn't offered for re-import). Returns [] when the adapter has * no on-disk store. */ export declare function discoverResumableSessionsForBot(cliId: CliId, cliPathOverride: string | undefined, activeSessions: Map, limit?: number): Promise; /** Import (resume) a stored session into the current topic: re-spawn the bot's * CLI via `--resume ` in `cwd`. Mirrors the manual resume path — * the worker owns the CLI (NOT an observe-adopt), so no `adoptedFrom` is set. */ export declare function startResumeImportSession(target: ResumableSession, ds: DaemonSession, deps: CommandHandlerDeps, larkAppId?: string): Promise; type ForkSubtopicResult = { ok: true; childSessionId: string; anchorId: string; link: string; } | { ok: false; error: string; orphanTopic: boolean; }; /** Fork the current session into a new sub-topic of the same topic group. * The session copy itself stays in worker-pool's generic `forkSession()`; * this layer only creates the Lark destination, supplies the first task turn, * and records display-only lineage for the parent panel. */ export declare function startForkSubtopicSession(taskText: string, parentDs: DaemonSession, message: LarkMessage, larkAppId?: string): Promise; //# sourceMappingURL=command-handler.d.ts.map