claude-code-weaverbird

module
v0.5.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 31, 2026 License: MIT

README

weaverbird

A statusline multiplexer for Claude Code. Claude Code exposes exactly one statusLine.command. weaverbird is that command. It reads the session JSON once, gathers structured records from many independent provider programs that do not know about each other, and weaves them into one status bar, owning color, ordering, separators, width degradation, and staleness itself.

Providers emit structured data and a semantic class, never pre-styled ANSI, so a bar assembled from tools that were never coordinated still reads as one coherent, themeable system.

The problem it solves

Claude Code allows a single statusline command, so tools that each want to be on the bar cannot coexist. The author has three that do: bloodhound (subscription quota), usage-gov (pacing alarm), and pigeon (inter-session messages). Only one can own the slot, and the other two go dark. weaverbird owns the slot once and composes all of them, plus anything else you drop in.

How it works

A provider publishes two things, the way a REST service publishes an OpenAPI document and endpoints:

  • a spec: a small capability document declaring the widgets it offers and their static metadata (id, kind, priority, icon, cache policy);
  • value records: one JSON line per widget with the current value, produced on demand.

Every widget declares a kind (text, meter, timestamp, or series) and the kind decides which further fields it carries: a text widget hands over an opaque string; a meter hands over a percentage and lets weaverbird compose and theme the bar; a timestamp hands over an instant and lets the host format and re-render elapsed time without asking the provider again; a series hands over a run of numbers and weaverbird draws the sparkline. A provider computes its own severity and sends it as class; weaverbird never derives one from a threshold.

weaverbird reads the spec once, then fetches values per render, in parallel, with a per-provider timeout and a last-known-good cache, and it only re-invokes a provider when that provider's declared cache policy says something changed. It maps class to color from one theme, lays the widgets out, and degrades gracefully when the terminal is narrow: a cheapest-move-first cascade shrinks a bar, swaps a text to its short form, drops icons, truncates, decomposes a meter's parts, and only then drops a widget outright.

Vocabulary: provider (a program or file that supplies data), widget (one addressable unit, identified by an id like bloodhound.week), kind (what a widget's data is), spec (a provider's capability document), value record (one widget's current value).

Install

weaverbird is a single Go binary.

make install                 # builds and installs the weaverbird binary
weaverbird install           # claims the statusLine slot in ~/.claude/settings.json (backs it up first)

weaverbird install also creates ~/.claude/weaverbird/providers/ and sets a refreshInterval so that time-based and alarm providers stay live while a session idles. Run weaverbird doctor at any time to confirm the slot is still owned by weaverbird and to see the discovered providers and their health.

Adding a provider

Drop a file into ~/.claude/weaverbird/providers/. That is the entire registration; no shared config is ever edited. An entry is one of:

  • a descriptor NN-name.provider.json that declares how to fetch the spec and values (an exec command you choose, a frozen file, or an inline document);
  • a bare executable that reads the session JSON on stdin and prints value records, for a quick script;
  • a *.ndjson file that some daemon writes on its own cadence.

The last two need nothing else: every widget id they mention gets a default declaration, a text widget re-read every render. To declare more than that (a meter, a cache policy, a second row), drop a spec document next to the entry, named for it: 50-clock and 50-clock.spec.json.

Reach for a descriptor when the provider is a tool you installed rather than a script you wrote. You have to name the command (bloodhound weaverbird value is only expressible there), and the spec then ships and upgrades with the tool instead of living in your config.

Disable one with weaverbird disable <name> (it appends .disabled); re-enable with enable.

Showing a widget only sometimes

A widget can be made conditional from config.json, alongside layout. This is yours to set, not a provider's: a provider says what it can show, layout says what you want shown, and when says under what circumstances.

{
  "layout": ["bloodhound.5h", "context.usage", "vcs.branch", "model.name"],
  "when": {
    "vcs.branch": { "unless": { "env": { "TERM_PROGRAM": "WarpTerminal" } } }
  }
}

That hides the git branch inside Warp, which already shows it, and keeps it everywhere else. A rule is if or unless over an env map, a session map (model, cwd, project_dir, and friends), or both. Values are globs, lists of globs, or true/false for "is it set".

A hidden widget is not merely invisible, it is never fetched: its provider is not asked for it, and if nothing else of that provider's is wanted, the provider is not run at all. weaverbird doctor prints every rule, the values it read, and whether the widget is shown or hidden.

The full contract is SPEC.md. A cookbook of every form in shell, Python, and Go is EXAMPLES.md. Machine-readable JSON Schemas are in schema/.

Writing a provider in Go

Import the provider helper package (pure standard library, no cobra required). It carries the wire types and lifts the whole subcommand into one call:

import wb "github.com/PeterSR/claude-code-weaverbird/provider"

var spec = wb.Spec{V: 1, Provider: "bloodhound", Icon: "🩸", Widgets: []wb.Widget{
    {ID: "bloodhound.week", Title: "Weekly quota", Kind: wb.KindText, Priority: 20,
     Text: &wb.TextKind{}, Cache: &wb.Cache{TTLSec: 10}},
}}

func value(s wb.Session, requested []string) ([]wb.Value, error) {
    // reuse your tool's existing data; compute your own class (no thresholds
    // to hand over); return one record per widget, omit to stay silent
    return []wb.Value{
        {ID: "bloodhound.week", FullText: "wk 38%", ShortText: "38%", Class: wb.ClassOK},
    }, nil
}

// inside your tool's `weaverbird` subcommand:
//   return wb.Dispatch(args, os.Stdin, os.Stdout, spec, value)

See EXAMPLES.md section 9 for the full subcommand.

Providers are not bundled

weaverbird ships no descriptors. A descriptor only declares how to reach a provider (the command to run for its spec and for its values) and weaverbird fetches the spec once per session and caches it, so there is nothing worth freezing into this repo. Registration lives on the machine that has the tools installed: drop a file into ~/.claude/weaverbird/providers/ (section 2 of SPEC.md) and that is the whole of it.

weaverbird depends on no particular provider. It is equally happy with one, with several, or with providers you write yourself.

The ambient widgets this README's examples use (vcs.branch, context.usage, model.name, and friends) come from weaverbird-contrib, a companion module: one binary carrying git, context, session, model, toolchain, kube, and container providers, registered with a single weaverbird-contrib install.

Theming

Color comes from a theme: a name plus a light and a dark palette. A palette is a flat map from key to color: classes color a widget's text; tones (<class>.tint, <class>.ink) are a lighter fill and a readable ink for that class; bar colors (bar.track, bar.ink, bar.fill.<class>) color a meter's bar when it renders as a track-style gauge. Any color value may be a reference to another key in the same palette, written @key, so a theme states a color once and reuses it.

The classes lie on two axes. ok, info, warn, danger, stale and neutral are severity, how alarming a reading is. gain and loss are polarity, which direction a reading moved: lines deleted are a loss, not a danger. A provider still never names a color, so green-for-more stays one theme's rendering of gain rather than something a provider hardcodes, and a theme that maps neither polarity key renders both as plain neutral.

The split matters for more than accuracy. "Color is reserved for what wants attention" is a rule about the severity axis, where the classes compete for one channel and coloring the calm ones is what devalues the alarming ones. Polarity makes no bid on that channel, so a restrained theme can spend color there freely. Two themes ship in the binary: vanilla (the default: classic ANSI yellow and red for warn and danger, dim for stale, nothing at all for ok, info and neutral, and green and red for gain and loss, which is that policy split in practice) and teal (a truecolor theme committing to hues for every class, including neutral, and keeping polarity deliberately less saturated than severity). Both declare bar colors for every class, so both can render a track-style gauge. Drop your own at ~/.claude/weaverbird/themes/<name>.json:

{
  "name": "sunset",
  "light": {
    "warn": "#e2892a", "warn.tint": "#f6d9a8",
    "danger": "#d1483a", "danger.tint": "#f0b3ac",
    "bar.track": "#e8e8e8", "bar.ink": "#1c1c1c",
    "bar.fill.warn": "@warn.tint", "bar.fill.danger": "@danger.tint"
  },
  "dark": { "warn": "#f0972f", "danger": "#e2564a", "bar.track": "#303030", "bar.ink": "#f0f0f0" },
  "separator": " | ",
  "provider_separator": " -- ",
  "bar": { "style": "track", "fill": "▓", "empty": "░" }
}

A key's value is "@key" (a reference, resolved against the same palette and appearance), "#rrggbb" (truecolor), a 256-color index, or a named ANSI color; a user theme of the same name overrides a built-in. A reference to a missing key, or a cycle, resolves as uncolored rather than erroring or hanging. separator and provider_separator are both optional, the plain text rendered, dim, between widgets, exactly as before. The theme-level bar object sets the bar's glyphs (fill/empty/ start/end, glyph style only) and the theme's own preferred style: glyph (default) draws fill/empty characters colored in the foreground; track draws the bar as a colored background so parts placed inside it sit on top of it, but track is only reachable when color is on and the active palette actually declares bar.* colors, else it falls back to glyph.

Which theme, appearance, and bar style are active is a pointer in ~/.claude/weaverbird/config.json (WEAVERBIRD_THEME/WEAVERBIRD_APPEARANCE override the first two for one process), changed with weaverbird theme use <name>, weaverbird theme appearance <auto|light|dark>, and weaverbird theme bar <glyph|track>, never by hand-editing colors. auto appearance resolves to light or dark from, in order: the env override, the config pointer, Claude Code's own ~/.claude/settings.json theme, $COLORFGBG, else dark. weaverbird theme list and weaverbird theme show [name] inspect the catalog. Full detail, including the exact appearance and bar style resolution order and the color value formats, is SPEC.md section 6.2.

To see a theme applied without waiting on a real Claude Code session, use weaverbird preview (below), which renders through the exact same theme and layout engine as the real bar.

Layout

By default weaverbird renders every enabled provider's widgets, in provider order, except any a provider marked "default": false: an opt-in, group-only widget that exists but does not clutter the bar unasked (handy for a chatty or niche widget most people never want to see). For coarse control over what goes on the bar and in what order, including pulling an opt-in widget in, a provider can declare named groups (curated bundles of its own widgets, e.g. bloodhound.quota), and you can point ~/.claude/weaverbird/config.json's layout at an ordered list of tokens:

{ "layout": ["bloodhound", "usage-gov.pace", "pigeon"] }

Each token is, in precedence order, a group id, a widget id, or a provider name (that provider's own implicit default group, every widget it declares, opt-in ones excluded). When layout is set, it is strict: only the widgets it resolves to render, in that order; anything not referenced is left off the bar, opt-in or not. An opt-in widget is still selectable, overriding its own default, either by naming its widget id directly or through a group that includes it. When layout is absent, the default view above applies. Manage it with weaverbird layout show (prints the resolved layout, by row, plus opt-in widgets available to add, what a strict layout omits, and any unknown token), weaverbird layout set <token...>, and weaverbird layout clear. Full detail, including the exact token precedence, is SPEC.md section 6.3 (and section 3.4 for opt-in widgets).

Commands

weaverbird                        read the session JSON on stdin and print the bar (the statusLine command)
weaverbird install                claim the statusLine slot (backs up settings first)
weaverbird doctor                 verify the slot is owned; list providers, widgets, and health
weaverbird list                   list discovered providers and their widgets
weaverbird enable <name>          re-enable a provider
weaverbird disable <name>         disable a provider without deleting it
weaverbird preview                render the bar in a normal terminal, outside a Claude Code session
weaverbird theme list             list built-in and user themes, marking the active one
weaverbird theme show [name]      print a theme's light and dark palettes
weaverbird theme use <name>       set the active theme
weaverbird theme appearance <auto|light|dark>   set the appearance pointer
weaverbird theme bar <glyph|track>   override how a meter's bar is drawn
weaverbird layout show            print the resolved layout: what renders, what's omitted, unknown tokens
weaverbird layout set <token...>  set the active layout
weaverbird layout clear           remove the layout, returning to the default (every widget)
weaverbird version                print the version

weaverbird preview runs the real render pipeline outside a session: by default it synthesizes a session payload from your environment (cwd, $CLAUDE_CODE_SESSION_ID or a stable placeholder, your real terminal width) and drives your actual providers, so you see your actual current bar. --input FILE (or - for stdin) replays a captured statusline payload instead. --demo bypasses the real providers and renders a fixed, clearly-synthetic widget set that exercises every class, the icon dedup, and the width cascade, so a theme can be fully evaluated even when real provider quota is low. --width, --theme, --appearance, and --layout (comma-separated tokens) override those settings for that one render only, without touching config.json.

Prior art

weaverbird's contract deliberately recombines established conventions rather than inventing a format: the class field for semantic state from Waybar, full_text/short_text width degradation and min_width from the i3bar protocol, the declaration-versus-render split and the caching and truncation rules from linesmith, the structured drop-in record from snackdriven/claude-statusline, manifest discovery and graceful degradation from betmoar/cc-status-plugin, HTML's <meter> (a measurement within a known range) for the kind of that name, and the typed content-block shape (a type discriminator beside a block named for it) from the Claude API for how a widget's kind decides its own nested field block. SPEC.md credits each in detail, and is explicit about where it departs: Waybar's threshold-to-class states map is deliberately not adopted, because a provider computes its own severity and sends it as class.

Scope

This is built to solve one real problem well, the author's three tools sharing one bar, and to be useful with a single provider and nobody else's. It is not framed as a standard. If the contract is clean enough that others find it worth implementing, that is earned later, not declared here.

Directories

Path Synopsis
cmd
weaverbird command
Command weaverbird is a statusline multiplexer for Claude Code: it reads Claude Code's session JSON once from stdin, fans it out to independent provider programs, and weaves the widgets they emit into one themed, width-aware status bar.
Command weaverbird is a statusline multiplexer for Claude Code: it reads Claude Code's session JSON once from stdin, fans it out to independent provider programs, and weaves the widgets they emit into one themed, width-aware status bar.
examples
quota command
Command quota is a tiny, complete weaverbird provider built on the public provider helper library.
Command quota is a tiny, complete weaverbird provider built on the public provider helper library.
internal
cache
Package cache implements weaverbird's session-keyed on-disk state: the cached spec per provider (fetched at most once per session) and the per-widget value cache that drives both the cache-policy gate (do not re-invoke within ttl_sec, or until invalidate.file changes) and the last-known-good fallback for a failing provider (SPEC.md section 5).
Package cache implements weaverbird's session-keyed on-disk state: the cached spec per provider (fetched at most once per session) and the per-widget value cache that drives both the cache-policy gate (do not re-invoke within ttl_sec, or until invalidate.file changes) and the last-known-good fallback for a failing provider (SPEC.md section 5).
ccinput
Package ccinput reads and leniently parses the JSON Claude Code pipes into its statusLine.command on stdin.
Package ccinput reads and leniently parses the JSON Claude Code pipes into its statusLine.command on stdin.
condition
Package condition evaluates the user's optional per-widget visibility rules from config.json's "when" key (SPEC.md section 6.4).
Package condition evaluates the user's optional per-widget visibility rules from config.json's "when" key (SPEC.md section 6.4).
engine
Package engine is weaverbird's hot path: for every enabled discovered provider it resolves the spec (cached, at most fetched once per session), applies the per-widget cache-policy gate, invokes the value source only when something is due, and merges the result into layout-ready widgets with class and icon already resolved.
Package engine is weaverbird's hot path: for every enabled discovered provider it resolves the spec (cached, at most fetched once per session), applies the per-widget cache-policy gate, invokes the value source only when something is due, and merges the result into layout-ready widgets with class and icon already resolved.
grouplayout
Package grouplayout resolves the user's optional layout (SPEC.md section 6.2): an ordered list of tokens, read from config.json, each a provider-declared group id, a widget id, or a provider name (that provider's own implicit default group, SPEC.md section 3), that together decide which widgets render and in what order.
Package grouplayout resolves the user's optional layout (SPEC.md section 6.2): an ordered list of tokens, read from config.json, each a provider-declared group id, a widget id, or a provider name (that provider's own implicit default group, SPEC.md section 3), that together decide which widgets render and in what order.
layout
Package layout owns kind composition, ordering, rows, icon placement, the cheapest-move-first width-degradation cascade, OSC 8 href wrapping, and joining widgets into the final bar text (SPEC.md section 6).
Package layout owns kind composition, ordering, rows, icon placement, the cheapest-move-first width-degradation cascade, OSC 8 href wrapping, and joining widgets into the final bar text (SPEC.md section 6).
registry
Package registry finds providers in the drop-in providers directory, classifies each entry (an explicit descriptor, a bare executable, or a bare records file), resolves its spec and value sources, and orders the result: the stable left-to-right base position for the bar (DESIGN section 5, SPEC.md section 2).
Package registry finds providers in the drop-in providers directory, classifies each entry (an explicit descriptor, a bare executable, or a bare records file), resolves its spec and value sources, and orders the result: the stable left-to-right base position for the bar (DESIGN section 5, SPEC.md section 2).
theme
Package theme owns class -> color: the theme catalog (the built-in vanilla and teal themes plus any user theme dropped into the themes directory), the light/dark appearance resolution, NO_COLOR handling, and the separator between widgets (SPEC.md section 6).
Package theme owns class -> color: the theme catalog (the built-in vanilla and teal themes plus any user theme dropped into the themes directory), the light/dark appearance resolution, NO_COLOR handling, and the separator between widgets (SPEC.md section 6).
version
Package version exposes build metadata, populated via -ldflags at build time.
Package version exposes build metadata, populated via -ldflags at build time.
Package provider is the public contract library for weaverbird provider authors.
Package provider is the public contract library for weaverbird provider authors.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL