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.2, 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.
The existing Feishu browser scenarios in test/e2e-browser/ run through
@midscene/test. Their Playwright and Midscene Web assertions are registered as
native YAML cases, preserving the original scenario behavior while adding
Midscene Test selection, retries, summaries, and unified reports.
Create storageState.json and provide the Feishu and MIDSCENE_MODEL_*
variables shown in .env.example, then run:
bun run test:midscene:typecheck
bun run test:e2e-browserEach invocation writes its replayable report below the gitignored
midscene_run/runs/<run-id>/ directory.
Lark WebSocket Events
|
Daemon (daemon.ts → core/ modules)
|-- im/lark/event-dispatcher: event routing
|-- im/lark/transport/connection: WebSocket connection and failed-state recovery
|-- 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
transport/connection.ts # WebSocket connection, proxy and failed-state recovery
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, excludingtest/e2e-browser/) — spawns real CLIs and runs sequentially. Opt-in only.- Feishu browser E2E (
test/e2e-browser/cases/*.yaml) — runs through@midscene/testagainst a shared daemon and logged-in browser session.
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:e2e-browser # Feishu browser cases through Midscene Test
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.