Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

bd-console

A Global Hub dashboard for beads (bd) issues, project docs, and your agent tmux sessions. Install it once per machine, register the repos you work in, and get one web UI for triage, docs, live tmux sessions, and scheduled prompts. Zero npm dependencies on the server: just Node (>=22) and the bd CLI. The frontend is a no-build Preact app with vendored dependencies — still no bundler, no build step.

What it does

  • Global Hub: one long-running server, many registered projects. Switch between repos from a single dashboard instead of running one server per repo.
  • Beads triage: every issue from .beads/issues.jsonl, including dependencies — status lanes (Ready · Active · Blocked · Done), optional epic grouping, a dependency map, and omnibar search. Claim, close, reopen, re-prioritize, label, link and re-parent issues inline.
  • Rich issue creation: type, priority, labels, acceptance criteria, parent epic, and assignee — not just quick one-line captures.
  • Comments: read and add comments on any issue through bd comment (always read live, never stale).
  • Docs: browse project markdown, either auto-discovered or pinned via a per-project docRoots list.
  • tmux sessions: see every tmux session and pane on the host the hub runs on — what's running, where, and a live scrollback preview.
  • Prompt scheduler: queue a literal prompt to be typed into an existing, named tmux session at a future time (see Scheduler below).
  • Themes: six presets (Synergy — the default, Default, Dracula, Nord, Gruvbox, Tokyo Night), each with light, dark, and auto (follows prefers-color-scheme) variants.
  • Keyboard shortcuts: vim/Gmail-like navigation for fast, hands-on-keyboard triage (see Keyboard shortcuts).

What's in the UI

This README covers install/CLI/config. For what the dashboard itself does — every page and route, the hub's sections, Console 2.0's header/canvas/Detail panel, the omnibar's full command list, end-to-end workflows (link two issues, delegate to tmux, pour a molecule, check usage, update bd, and more), and a "recently added" rundown of link types/graph-links/molecules/ usage analytics/bd-version surfacing/default epics — see docs/feature-map.md.

Install

You need:

  • Node >=22 (required — the scheduler uses node:sqlite, which landed in Node 22)
  • the bd CLI on your PATH
  • optionally, tmux on the same host, if you want the tmux/scheduler features (the hub degrades gracefully without it — see Scheduler)

Option A: clone this repo

No npm install step is required because bd-console has no npm dependencies.

git clone https://github.com/formless63/bd-console.git ~/code/bd-console
cd ~/code/bd-console
node serve.mjs start

Option B: install the CLI globally from GitHub

npm install -g git+https://github.com/formless63/bd-console.git
bd-console start

Either path gives you the bd-console command (and bd-console-init, an optional guided per-repo setup helper — see Guided per-repo setup).

Quick start

# 1. install (see above), then start the hub — installed once per machine
bd-console start

# 2. first run on a TTY walks you through a LAN vs VPS setup prompt (see
#    Security posture below); non-interactive first runs apply safe-ish
#    defaults and log what they picked

# 3. register the repos you want on the dashboard
bd-console add ~/code/my-project
bd-console add .          # registers the repo at (or above) the cwd

# 4. open the dashboard
bd-console status         # prints the URL, e.g. http://localhost:4180

Each registered project needs a working beads database (.beads/) at its root. If a target repo doesn't have one yet:

cd ~/code/my-project
bd init
bd export -o .beads/issues.jsonl

There is no single-repo server mode anymore — bd-console always runs as one Global Hub, and every project it can see is something you explicitly registered with add.

CLI reference

bd-console                  run the hub in the foreground (Ctrl-C to stop)
bd-console start            (re)start the hub as a supervised background process
bd-console stop             stop it, however it's currently supervised
bd-console status           pid + port-reachability + supervision report
bd-console add <path>       register a project with the hub (defaults to cwd,
                             walking up to the nearest .beads/)
bd-console remove <id>      unregister a project
bd-console list             list registered projects
bd-console settings         interactive settings walkthrough
bd-console settings list    print effective host/port/token/persist + source
bd-console settings set <key> <value>
bd-console settings unset <key>
bd-console update           self-upgrade in place (see below)
bd-console update --dry-run print the detected install flavor + planned
                             command(s) without running them

Global flags (--port <n>, --host <h>) work on the bare run and on start/stop/status, and take precedence over everything else (see Configuration). A bare trailing number is accepted as a back-compat shorthand for --port on those same commands.

start semantics

start always supersedes any existing deployment rather than silently no-oping:

  1. stops a previous plain-daemon process (tracked by pid file), if any
  2. stops an active systemd --user unit, if any
  3. as a final catch-all, checks whether something is still answering on the configured port and, if it identifies itself as bd-console via /api/meta, takes over the port too

It refuses to kill a process on the port that does not look like bd-console (you'll get a "port already in use" error instead — free it manually or pick a different --port). Once the old deployment is torn down, start brings up a fresh one and polls /api/meta until the replacement actually answers, then prints the dashboard URL and how it's supervised. If the new instance never becomes ready, it prints the tail of the log (systemd journal or the plain log file) and exits nonzero.

status semantics

Reports three independent things: whether a pid is alive, whether the configured port is actually reachable (a live /api/meta fetch, not just "process exists"), and how the running instance is supervised — systemd, plain (pid-file daemon), foreground (answering the port but not tracked by either, e.g. you ran bare bd-console yourself), or none.

update semantics

bd-console update detects how it's installed and upgrades in place:

  • git-clone install (the package root is inside a git work tree) → git pull --ff-only. A dirty working tree is never touched — no stash, no reset. update aborts with instructions to commit or stash yourself, then retry.
  • npm-global install (anything else) → npm install -g git+https://github.com/formless63/bd-console.git

If bd-console was running before the update, it's restarted afterward through the same superseding start path, so the new code goes live immediately. --dry-run prints the detected flavor and exact command(s) without running them.

Versioning

The version in package.json is the installed bd-console version. A GitHub Actions workflow automatically increments its patch component after each push to main. If the push already changes the version, the workflow respects that deliberate major/minor/patch choice instead of incrementing it again.

For an intentional release bump, maintainers can run one of:

npm run version:bump -- patch
npm run version:bump -- minor
npm run version:bump -- major

The command only updates package.json; it does not implicitly commit, tag, publish, or push. The workflow can also be dispatched manually with the same three choices. This keeps GitHub-installed copies distinguishable even though bd-console is not yet published to the npm registry.

Persistence (systemd)

On Linux, when systemctl --user is usable, start installs/refreshes a systemd --user unit (bd-console.service) and runs loginctl enable-linger so the hub survives logout and reboot — this is the default (persist: true) whenever that probe succeeds. Manage the unit directly if you want to:

systemctl --user status bd-console.service
systemctl --user stop bd-console.service
systemctl --user restart bd-console.service

Opt out with bd-console settings set persist false (or the BD_CONSOLE_PERSIST=0 env var), which makes start use a plain detached background process (pid-file tracked) instead. On non-Linux platforms, or when systemctl --user isn't usable, persist defaults to false automatically.

Configuration

Global settings (host, port, token, persist) live in ~/.config/bd-console/config.json (override the directory with BD_CONSOLE_CONFIG_DIR). Precedence, highest wins:

CLI flags (--port, --host) > env vars > config.json > built-in defaults

Built-in defaults: 0.0.0.0:4180, no token, persist auto-detected.

Inspect and edit with bd-console settings:

bd-console settings list         # effective value + source (flag/env/config/default) per key
bd-console settings set host 0.0.0.0
bd-console settings set port 4180
bd-console settings set token a-shared-secret
bd-console settings unset token
bd-console settings set persist false
bd-console settings                # interactive walkthrough of all four keys

config.json looks like:

{
  "host": "0.0.0.0",
  "port": 4180,
  "token": "a-shared-secret",
  "persist": true
}

All keys are optional; omit any you want left at its default.

Environment overrides

BD_CONSOLE_HOST             # overrides host
BD_CONSOLE_PORT             # overrides port
BD_CONSOLE_TOKEN            # overrides token
BD_CONSOLE_TRUSTED_HOSTS     # optional comma-separated public/proxy hostnames
BD_CONSOLE_PERSIST=0|1      # overrides persist
BD_CONSOLE_CONFIG_DIR       # relocates ~/.config/bd-console entirely
BD_CONSOLE_SCHED_INTERVAL   # scheduler poll interval in ms (default 15000)
BD_CONSOLE_TERMIX_URL       # overrides termix.baseUrl (see below)
BD_CONSOLE_TERMIX_TOKEN     # overrides termix.token
BD_CONSOLE_TERMIX_HOST_ID   # overrides termix.hostId
BD_CONSOLE_IDLE_DAYS        # days of terminal silence before a session is
                            # marked "unattended" (default 7 — see below)

Session health: memory and unattended sessions

On Linux, the session list also reports what each tmux session's process tree is holding in resident memory, and the hub carries a host-level chip: how much memory is still available, whether this machine has any swap, and how much of the total the detected agent sessions account for. A machine with swap gets slow when it fills up and a human notices; a machine without swap has no such stage — the kernel's OOM killer simply picks the largest process, which on this project's own host has twice ended a tmux session and everything running in it. Amber/red chips carry the state in words (memory tight, 1.2 GB high), never in color alone.

A session is marked unattended when nobody has attached to it and it has printed nothing for BD_CONSOLE_IDLE_DAYS days (default 7), while its processes are still accruing CPU time. That combination — total terminal silence plus live work — is what distinguishes a forgotten agent quietly spending API quota from a parked shell (no CPU) or a session you drive from this console (which produces output). The tooltip always names the evidence. Everything here degrades to absent — no /proc, no Linux, no tmux, no feature — rather than to an error.

Termix: open a tmux session in the browser

If you run Termix (a self-hosted web SSH / terminal manager), every tmux session row in the hub, the Terminal sessions view and Console 2.0 can carry a Termix link that opens that session in Termix's web terminal. Optional; nothing appears until you configure it.

Three settings, all under termix in config.json:

bd-console settings set termix.baseUrl https://termix.example.com
bd-console settings set termix.token   tmx_<64 hex chars>
bd-console settings set termix.hostId  3
  • termix.baseUrl — the address you browse Termix at. The Docker image fronts all of its internal services with one nginx on a single published port (8080 by default), so this is that one origin — never the internal 3000x service ports.
  • termix.token — a Termix API key, created inside Termix under Admin Settings → API Keys (format tmx_ + 64 hex characters). It is used by exactly one thing: the Look up hosts button on bd-console's Settings page, which lists your Termix hosts so you can pick the right hostId without hunting for it. It is never sent to your browser and never appears in a link, and bd-console makes no other request to Termix — not on startup, not on save, not on a poll.
  • termix.hostId — which Termix host entry is this machine. bd-console cannot infer this. Without it the session links still work but only open Termix; with it they attach the session directly.

The link is an ordinary URL (…/?view=terminal&hostId=<id>&tmuxSession=<name>) that your browser opens in a new tab. Termix requires its own login session for it, so the first click of a browsing session may land on Termix's sign-in page — the link's tooltip says so.

Deep links are an internal Termix contract, not a documented API. They work as of Termix's current main; a future Termix release could change them. The bd-console side is one function (lib/termix.mjs) if it ever needs updating.

Per-project bd-console.json

A registered workspace may still have a bd-console.json at its root, but its job has shrunk: only docRoots is honored now. host, port, and token keys in a per-project file are inert — those all come from the global config.json / env / flags described above. See bd-console.json.example.

{
  "docRoots": ["docs", ".planning"]
}
  • docRoots: limits and orders which markdown folders the Docs view shows for that project. Omit it to auto-discover every *.md under the workspace (skipping node_modules, .git, .next, dist, .beads, and similar).

Guided per-repo setup

bd-console-init (or node scripts/init.mjs from a clone) is an optional helper for onboarding a single repo: it confirms .beads/ exists, refreshes .beads/issues.jsonl, registers the repo with the hub (the same as running bd-console add), writes bd-console.json if one doesn't exist yet, and can update AGENTS.md / CLAUDE.md with bd-console guidance.

bd-console-init --repo ~/code/my-project --apply-agent-docs
bd-console-init --repo ~/code/my-project --apply-agent-docs --create-missing-agent-docs

--repo here is a bd-console-init flag only — it picks which repo to initialize/register. It has no equivalent on bd-console/bd-console start (see docs/upgrading.md if you're coming from the old per-repo --repo server flag). (Linux users): add --install-service to install and start the shared hub's systemd user unit as part of the same step.

Security posture

bd-console's first run on a TTY (bare bd-console, or bd-console start before any config.json exists) asks where you'll access the dashboard from, because the fallback default bind is 0.0.0.0 — not 127.0.0.1 — and that shouldn't be applied silently:

  • This machine / home LAN → bind 0.0.0.0, open writes recommended. This is the common case for a box you trust everyone on your LAN to reach.
  • VPS / internet-exposed → bind to a private or tailnet interface first (the prompt lists your non-loopback IPv4 addresses, plus 127.0.0.1); a random write-gating token is generated only as a last resort if you choose to bind 0.0.0.0 anyway on a box like this.

If a box is ever exposed publicly, put an authenticating reverse proxy in front of it (e.g. Pangolin's auth) rather than relying on the token alone. The token gates write endpoints against casual/accidental use; it is not a substitute for real authentication on an internet-facing host.

Non-interactive first runs (no TTY — systemd, scripts, CI) skip the prompt, apply the 0.0.0.0:4180/no-token defaults, and log one line saying so. Change any of this later with bd-console settings.

Other invariants, unchanged from the original design:

  • Writes go through bd via execFile with an args array — no shell interpolation, ever.
  • Issue IDs and labels are format-validated before every write.
  • Browser writes must be same-origin JSON requests. Direct LAN hostnames, interface addresses, and a locally terminating Pangolin/newt proxy work without setup; unusual remote proxy topologies can add their public names with BD_CONSOLE_TRUSTED_HOSTS=name1,name2.
  • File/session creation and doc/formula reads and writes are confined to registered workspace roots using real-path checks, including symlinks.
  • Sensitive host reads (settings, scheduler prompts, saved prompts, tmux inventory/previews, and usage details) are token-gated when an optional token is configured. They remain open in the intentional tokenless LAN mode.
  • Tokens are accepted only through the x-bd-token header, never a query string. The browser UI stores an optional token in localStorage and sends that header automatically.

Scheduler

The scheduler queues a prompt to be typed into an already-running, named tmux session at a scheduled time — think "wake this agent up and give it its next instruction at 7am" or "resume this session after my laptop sleeps through the night."

What it does not do, by design:

  • it never creates or attaches a tmux session for you — the session named in the job must already exist when the job fires
  • it never runs a command like claude -p directly; it only re-uses the same literal tmux send-keys injection the tmux pane preview/UI uses, so the "prompt" always lands as literal keystrokes in an interactive session, never as something the scheduler interprets or executes itself

A job that fires against a session that no longer exists (or never existed) ends up failed, not silently dropped — check GET /api/schedule for job status (pending / sent / failed / cancelled) and error.

Agent type and "server mode"

GET /api/tmux labels each session and pane with the agent CLI it detected (agent: claude / codex / gemini / kimi / null) and whether that pane can actually be prompted (promptable, plus a mode of interactive / server / shell / unknown and a human reason). Detection reads tmux's own fields first, then the pane process's real /proc argv and its children, with the pane title as a last-resort tiebreak — pane_current_command alone can't tell an interactive claude from a claude rc remote-control host, and kimi-code looks identical whether it's a TUI or a web server.

Sessions in server mode (claude rc, codex app-server, a kimi web server, …) are still listed — they're real work on the host — but their prompt/delegate controls are disabled with the reason attached, and POST /api/tmux/send answers 409 instead of typing into a process that will never read it. That refusal only happens when the verdict came from the process's own argv; add "force": true to the body to override it. Anything less certain stays promptable, so detection can only ever add refusals it can prove.

API surface, hub-level (not project-scoped):

GET  /api/tmux                    list tmux sessions/panes (token-gated when configured)
                                  (+ detected agent / mode / promptable)
GET  /api/tmux/preview?session=&lines=   scrollback preview (token-gated when configured)
POST /api/tmux/send                type a prompt now: {session, text, force?}
GET  /api/schedule                 list jobs (token-gated when configured)
POST /api/schedule                 create a job: {prompt, session, runAt}
POST /api/schedule/cancel          cancel a still-pending job: {id}

The scheduler polls for due jobs every BD_CONSOLE_SCHED_INTERVAL milliseconds (default 15000). It requires node:sqlite (bundled with Node 22+); if it's ever unavailable, the endpoints above return 501 instead of erroring, and the rest of the hub keeps working normally. Likewise, if tmux isn't installed on the host, /api/tmux reports { available: false, sessions: [] } rather than failing.

Themes and keyboard shortcuts

Six theme presets — Synergy, Default, Dracula, Nord, Gruvbox, Tokyo Night — each with light, dark, and an "auto" mode that follows prefers-color-scheme. Synergy is the default for first-time visitors (a void-black/near-white neon green + electric purple theme, with Space Grotesk/Manrope typography); anyone who has already picked a preset keeps their existing choice untouched. Pick one from the theme switcher in the top bar; your choice is remembered in localStorage and applied before first paint (no flash of the wrong theme).

Shortcut Action
/ Focus the search field (the omnibar, inside a project)
i Open the New issue dialog
Esc Close the open modal dialog or remove focus

These are ignored while you're typing into a form field or text input. Console 2.0's omnibar has its own shortcuts on top of these — see docs/feature-map.md §5.

The dashboard is hash-routed (#/, #/p2/<projectId>, plus #/tmux, #/schedule, #/settings and #/learn), so deep links work even served as static files. The older #/p/<projectId> project view was retired; those URLs now redirect to #/p2/<projectId>, so old bookmarks keep working.

Verification

Local checks:

npm run check
npm run smoke
npm_config_cache=/tmp/bd-console-npm-cache npm pack --dry-run
  • npm run check runs node --check over every server, script, and frontend module.

  • npm run smoke spins up a temporary beads repo and a temporary BD_CONSOLE_CONFIG_DIR, starts bd-console against them, and exercises the hub end to end: registry add/list, issues/docs/comments, inline edits, quick capture, rich issue creation + epics, the tmux API, the scheduler (create/list/cancel + a tick-driven failure case), daemon start supersede behavior, systemd unit-file generation, update --dry-run, settings set/list/unset, and the non-TTY first-run defaults path. It never touches your real ~/.config/bd-console, your real ~/.claude / ~/.codex / ~/.kimi-code / ~/.gemini, or a real systemd user session.

  • The suite is split by domain under scripts/smoke/, with scripts/smoke.mjs as the single entry point. Run one domain while you iterate on it:

    npm run smoke -- usage        # or: node scripts/smoke.mjs usage
    node scripts/smoke.mjs docs formulas
    node scripts/smoke.mjs --list # the domain names

    A plain npm run smoke runs every domain except the opt-in ones, which --list marks.

  • node scripts/smoke.mjs browser is the one opt-in domain. It drives a real headless Chrome over the DevTools Protocol — using Node's built-in WebSocket client, so it adds no npm dependency — and asserts only the things Node cannot see: that no route widens past the viewport at 1280px or 390px, that the Detail slide-over opens on screen, traps focus, and never scrolls the shell (the bd-console-clb regression, checked across all three close paths), and that the panel actually receives taps rather than the content behind it. It is excluded from the default run because booting a browser costs ~19s against a suite that otherwise finishes in well under a minute; run it before shipping changes to layout, overflow, position or z-index. With no Chrome installed it prints a smoke skip (...) line and exits cleanly. Point it at a specific binary with BD_CONSOLE_CHROME=/path/to/chrome.

  • npm pack --dry-run confirms the published tarball contains the expected files (the smoke suite is not published — only scripts/init.mjs ships).

Documentation site

This repo also includes a no-build static docs site in site/.

npm run docs:serve

For deployment details, see docs/docs-site.md. If you're upgrading from an older single-repo install of bd-console, see docs/upgrading.md.

Export freshness

Comments are always read live via bd comments --json, so they never go stale. The issue list comes from .beads/issues.jsonl; the server auto-refreshes that export whenever it's missing or older than .beads/last-touched, and the dashboard shows the current sync state in the top bar.

If you mutate issues outside the dashboard — bd create, bd update, bd close, bd dep, bd label, or similar — and want the change to show up before the next auto-refresh, run:

bd export -o .beads/issues.jsonl

Ideas and triage conventions

Quick capture and rich issue creation both default new issues into the triage label if you don't set one. Treat triage as an inbox, not a final classification:

  • Use triage for raw captured work that still needs sorting.
  • Add focused labels after review.
  • For doc-originated ideas, use a doc:<path> label so the source document stays recoverable, until doc-anchored capture becomes a first-class UI flow.

Agent integration

bd-console-init --apply-agent-docs inserts a short bd-console block into AGENTS.md / CLAUDE.md covering how to start the hub, that .beads/issues.jsonl should be refreshed after non-UI beads mutations, the triage / doc:<path> conventions, and how to verify the dashboard is reachable. This repo's own AGENTS.md and CLAUDE.md show the intended shape.

Troubleshooting

  • no .beads/ found at <path> when running bd-console add The path (or nothing above it, walking up from cwd) has a .beads/ directory. Run bd init there first, or point add at the right path.
  • Port <n> is already in use by a process that does not look like bd-console start refuses to kill unknown processes on the configured port. Free it manually, or run with a different --port.
  • start says it started, but the dashboard doesn't load Run bd-console status — it checks real port reachability, not just whether a pid is alive. If it's not reachable, check the log (~/.config/bd-console/console.log, or journalctl --user -u bd-console.service under systemd supervision) — start also tails this automatically on failure.
  • A project I registered doesn't show up / 404s Confirm it's actually registered: bd-console list. Re-add it with bd-console add <path> if not.
  • Issue list looks stale Run bd export -o .beads/issues.jsonl inside that project (see Export freshness); the server also auto-refreshes it on access when stale.
  • Comments show up but new issues or status changes don't Comments are always live; the issue list comes from the JSONL export.
  • Docs list is noisy in a large repo Set docRoots in that project's bd-console.json.
  • The scheduler / tmux API returns 501 You're on Node <22 (node:sqlite isn't available). Upgrade Node — the rest of the hub still works without it.
  • A scheduled job shows status failed Check its error — almost always "tmux session not found": the named session wasn't running when the job's runAt arrived. The scheduler never creates sessions for you.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages