git clone https://github.com/deepcoldy/botmux.git
cd botmux
bun install --frozen-lockfile
bun run build
# Run directly (no PM2)
bun run daemon
# Or with PM2
bun run daemon:start
bun run daemon:logsEvery code change requires
bun run buildthenbun run daemon:restart.
Package manager is bun (
packageManager: bun@1.4.0, lockfilebun.lock).trustedDependenciesinpackage.jsonmust keepnode-pty: bun does not run dependency lifecycle scripts by default, andnode-ptyneeds its install hook to buildbuild/Release/pty.node— without it the PTY layer is dead and the compiled single-file binary cannot be built. The list is deliberately just["electron","node-pty"](identical to theonlyBuiltDependenciesit replaced) — don't "complete" it by adding esbuild: its binary comes from the@esbuild/<platform>package, not its postinstall.This is about building botmux from source. How end users install botmux is a separate matter —
pnpm i -g botmuxis still a supported install path (seeInstallKindinsrc/utils/install-diagnostics.ts), so don't "convert" those to bun.
Lark WebSocket Events
|
Daemon (daemon.ts → core/ modules)
|-- im/lark/event-dispatcher: event routing
|-- im/lark/card-handler: card interactions
|-- core/worker-pool: worker process pool
|-- core/command-handler: slash commands
|-- core/session-manager: session lifecycle
|-- core/scheduler: cron scheduling
|
Worker (worker.ts) -- forked per session
|-- adapters/cli/*: CLI adapters (Claude Code / Codex / Gemini / OpenCode)
|-- adapters/backend: PtyBackend or TmuxBackend
|-- utils/idle-detector: idle detection
|-- HTTP + WebSocket: xterm.js web terminal
|-- Headless xterm: screen capture for streaming cards
|-- IPC: daemon communication
|
AI Coding CLI (interactive TTY)
|-- Auto-installed Skills (~/.claude/skills/, ~/.gemini/skills/, ~/.config/opencode/skills/)
|-- ~/.botmux/bin/botmux wrapper on PATH → `botmux send/schedule/bots/thread` subcommands
|
Lark API
|-- Replies, reactions, card updates, DMs
src/
cli.ts # CLI entry (setup/start/stop/restart/logs/list/delete + send/bots/schedule/thread subcommands)
daemon.ts # Daemon orchestrator
worker.ts # Worker: CLI + PTY management, web terminal
bot-registry.ts # Multi-bot registry
config.ts # Environment config
types.ts # IPC message types
adapters/
cli/
types.ts # CliAdapter interface, CliId type
registry.ts # Adapter factory + resolveCommand
claude-code.ts # Claude Code adapter
codex.ts # Codex adapter
gemini.ts # Gemini CLI adapter
backend/
types.ts # SessionBackend interface
pty-backend.ts # node-pty backend
tmux-backend.ts # tmux backend (persistent sessions)
core/
types.ts # DaemonSession core type
worker-pool.ts # Worker process pool
command-handler.ts # Slash command processing
session-manager.ts # Session lifecycle + path resolution
cost-calculator.ts # Token usage & cost estimation
scheduler.ts # Cron scheduling (natural language parsing)
im/
types.ts # ImAdapter interface (multi-IM abstraction)
lark/
client.ts # Lark API wrapper
event-dispatcher.ts # Lark WebSocket event routing
card-handler.ts # Lark card interaction handling
card-builder.ts # Lark interactive card builders
message-parser.ts # Lark event message parsing
skills/
definitions.ts # Built-in Skill markdown (botmux-send/schedule/bots/thread-messages)
installer.ts # Syncs skills into each CLI's native skills dir
services/
session-store.ts # Session persistence (JSON)
schedule-store.ts # Scheduled task persistence
message-queue.ts # Per-thread JSONL message queue
project-scanner.ts # Git repo/worktree discovery
utils/
idle-detector.ts # CLI idle detection
terminal-renderer.ts # Headless xterm renderer (screen capture & TUI filtering)
logger.ts # Logging utility
botmux exposes its Lark-interaction surface as CLI subcommands
(botmux send, botmux schedule, botmux bots,
botmux thread messages) paired with auto-installed Skills that
teach the agent when/how to use them.
Runtime setup per CLI worker spawn (see src/core/worker-pool.ts):
ensureCliSkills(cliId)— writessrc/skills/definitions.tscontent into the CLI's native skill dir (~/.claude/skills/,~/.gemini/skills/,~/.config/opencode/skills/). Synchronous, idempotent per lifecycle.- Worker
PATHis prepended with~/.botmux/bin, which contains abotmuxshell wrapper written by the daemon at startup (points at the running daemon'sdist/cli.js— always in sync). --append-system-promptflag injects the routing instruction ("user reads Lark, not terminal — usebotmux sendfor user-facing content") into each CLI session.- Every user message carries a per-message hint (
[回复请用 botmux send]) appended inbuildFollowUpContentto keep the instruction near the attention window even in long conversations.
| Subcommand | Description |
|---|---|
botmux send [content] |
Send message to current thread (stdin / heredoc / --content-file; --images / --files / --videos / --mention flags) |
botmux bots list |
List bots in current chat with their open_ids |
botmux thread messages [--limit N] |
Fetch thread message history (JSON) |
botmux schedule add <schedule> <prompt> |
Create scheduled task bound to current thread |
botmux schedule list/remove/pause/resume/run |
Manage tasks |
All agent-facing subcommands auto-detect session context by walking the
process tree looking for a CLI-pid marker written by the worker
({dataDir}/.botmux-cli-pids/{pid}). Works across every CLI that can
spawn child processes — no extra protocol support required from the CLI.
- Create a new file in
src/adapters/cli/, implementing theCliAdapterinterface - Add the new ID to the
CliIdtype insrc/adapters/cli/types.ts - Add a case to the switch in
src/adapters/cli/registry.ts - Set
"cliId": "<new-id>"inbots.jsonto use it
Full checklist — display names, setup choices, README updates: see
src/adapters/cli/CLAUDE.md.
The CliAdapter interface requires:
| Method / Property | Description |
|---|---|
id |
Unique CLI identifier |
resolvedBin |
Path to the CLI binary |
buildArgs() |
Construct CLI launch arguments |
writeInput() |
Write user input to the PTY (handles multi-line, Enter key timing) |
skillsDir |
Absolute path to the CLI's skills directory (optional; Skills only installed when set) |
completionPattern |
Regex to detect when a turn is complete (optional) |
readyPattern |
Regex to detect when the CLI is ready for input (optional) |
systemHints |
System-level hints injected into the CLI (optional) |
altScreen |
Whether the CLI uses alternate screen mode |
modelChoices |
Curated model candidates surfaced in botmux setup (optional). Set when the CLI accepts a model flag (consumed in buildArgs via the model opt); omit when the CLI has no --model concept — setup then skips the model prompt for that CLI |
Tests are split into two Vitest projects with different execution profiles
(see vitest.config.ts):
unit(*.test.ts) — pure, filesystem-mocked or temp-dir-isolated. Runs with file parallelism on (one process per file). This is whatbun run testruns, so the default is fast (~10s) and needs no real CLI binary or browser.e2e(*.e2e.ts) — spawns real CLIs / drives the Feishu web UI through a shared daemon, so files run sequentially. Opt-in only.
bun run test # Unit tests only — parallel, ~10s (default)
bun run test:all # Unit + E2E (needs real CLIs / browser session)
bun run test:e2e # All *.e2e.ts (sequential)
bun run test:codex # Codex input E2E
bun run test:gemini # Gemini CLI input E2E
bun run test:bench # Benchmark the unit suite (see docs/test-benchmark.md)
bun run test:bench --compare # serial vs parallel vs parallel+time-scale tableSpeed knob: adapter
writeInput()waits real wall-clock time to confirm a submit (poll the CLI history/transcript).BOTMUX_TIME_SCALE(read bysrc/utils/timing.ts, default1= unchanged in production) multiplies every such delay. Filesystem-mocked unit tests set it small to collapse those waits. Seedocs/test-benchmark.md.
- Treat a test fixture's
readyhandshake as a happens-before barrier, not a progress log: publish it only after every handler, listener, resource, and durable state needed by the parent's next action is installed. - Establish preconditions and postconditions through the exact observable event or state under assertion. Do not infer sibling or downstream telemetry from a lifecycle event unless its contract explicitly orders them. Fixed sleeps may model intentional timing or bound a hang, but must not stand in for readiness.
- Make scheduler-sensitive ordering deterministic in the fixture. Widen a timeout only for proven slow-but-progressing work, and never use retries to mask a missing barrier or known race.
- Keep every wait bounded and make timeout errors identify the unmet condition.