Skip to content

Latest commit

 

History

History
239 lines (205 loc) · 10.9 KB

File metadata and controls

239 lines (205 loc) · 10.9 KB

Contributing to botmux

Development Setup

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:logs

Every code change requires bun run build then bun run daemon:restart.

Package manager is bun (packageManager: bun@1.4.2, lockfile bun.lock). trustedDependencies in package.json must keep node-pty: bun does not run dependency lifecycle scripts by default, and node-pty needs its install hook to build build/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 the onlyBuiltDependencies it 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 botmux is still a supported install path (see InstallKind in src/utils/install-diagnostics.ts), so don't "convert" those to bun.

Midscene Test · Feishu browser E2E

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-browser

Each invocation writes its replayable report below the gitignored midscene_run/runs/<run-id>/ directory.

Architecture

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

Project Structure

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

CLI-Agent Interaction (Skills + CLI subcommands)

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):

  1. ensureCliSkills(cliId) — writes src/skills/definitions.ts content into the CLI's native skill dir (~/.claude/skills/, ~/.gemini/skills/, ~/.config/opencode/skills/). Synchronous, idempotent per lifecycle.
  2. Worker PATH is prepended with ~/.botmux/bin, which contains a botmux shell wrapper written by the daemon at startup (points at the running daemon's dist/cli.js — always in sync).
  3. --append-system-prompt flag injects the routing instruction ("user reads Lark, not terminal — use botmux send for user-facing content") into each CLI session.
  4. Every user message carries a per-message hint ([回复请用 botmux send]) appended in buildFollowUpContent to keep the instruction near the attention window even in long conversations.

CLI subcommands (agent-facing)

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.

Adding a New CLI Adapter

  1. Create a new file in src/adapters/cli/, implementing the CliAdapter interface
  2. Add the new ID to the CliId type in src/adapters/cli/types.ts
  3. Add a case to the switch in src/adapters/cli/registry.ts
  4. Set "cliId": "<new-id>" in bots.json to 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

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 what bun run test runs, so the default is fast (~10s) and needs no real CLI binary or browser.
  • e2e (*.e2e.ts, excluding test/e2e-browser/) — spawns real CLIs and runs sequentially. Opt-in only.
  • Feishu browser E2E (test/e2e-browser/cases/*.yaml) — runs through @midscene/test against 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 table

Speed knob: adapter writeInput() waits real wall-clock time to confirm a submit (poll the CLI history/transcript). BOTMUX_TIME_SCALE (read by src/utils/timing.ts, default 1 = unchanged in production) multiplies every such delay. Filesystem-mocked unit tests set it small to collapse those waits. See docs/test-benchmark.md.

Timing-Sensitive Test Contract

  • Treat a test fixture's ready handshake 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.