{"id":51319837,"url":"https://github.com/cdrrazan/kamandar","last_synced_at":"2026-07-01T12:02:01.026Z","repository":{"id":367985457,"uuid":"1283037510","full_name":"cdrrazan/Kamandar","owner":"cdrrazan","description":"Personal GitHub sprint command center!","archived":false,"fork":false,"pushed_at":"2026-06-28T14:21:06.000Z","size":60,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-28T16:10:41.566Z","etag":null,"topics":["command-center","github","ruby"],"latest_commit_sha":null,"homepage":"https://kamandar.byaru.com","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cdrrazan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-28T13:30:52.000Z","updated_at":"2026-06-28T14:22:15.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/cdrrazan/Kamandar","commit_stats":null,"previous_names":["cdrrazan/kamandar"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/cdrrazan/Kamandar","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cdrrazan%2FKamandar","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cdrrazan%2FKamandar/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cdrrazan%2FKamandar/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cdrrazan%2FKamandar/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cdrrazan","download_url":"https://codeload.github.com/cdrrazan/Kamandar/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cdrrazan%2FKamandar/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35005413,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-01T02:00:05.325Z","response_time":130,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["command-center","github","ruby"],"created_at":"2026-07-01T12:01:56.477Z","updated_at":"2026-07-01T12:02:01.018Z","avatar_url":"https://github.com/cdrrazan.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n\u003cimg src=\"assets/logo-web.png\" alt=\"Kamandar\" width=\"132\" /\u003e\n\n# Kamandar\n\n### Take aim at your GitHub work queue.\n\n***Kamandar*** (کمان‌دار) is Persian for *archer* — one who draws the bow and\nfinds the target. A personal GitHub command center: one command shows what you\nowe, what you're building, what's assigned, and what's gone quiet — as a\ncolored terminal report, a full-screen Matrix dashboard, a self-contained\nbrowser page, or a live local web app. No backend; the only network listener is\nthe opt-in `--serve`, bound to localhost.\n\n\u003cbr\u003e\n\n![Ruby](https://img.shields.io/badge/Ruby-3.2%2B-CC342D?logo=ruby\u0026logoColor=white)\n![Dependencies](https://img.shields.io/badge/dependencies-stdlib%20only-2ea44f)\n![Tests](https://img.shields.io/badge/tests-254%20passing-2ea44f)\n![Serverless](https://img.shields.io/badge/serverless-no%20server%20·%20no%20DB%20·%20no%20OAuth-0969da)\n![License](https://img.shields.io/badge/license-MIT-blue)\n![PRs welcome](https://img.shields.io/badge/PRs-welcome-ff69b4)\n\n\u003cbr\u003e\n\n![Kamandar live web app (--serve)](dashboard.png)\n\n\u003csub\u003eThe live web app (`ruby lib/kamandar.rb --serve`). Pure HTML + CSS — sidebar tabs, two work boxes, and pagination, no JavaScript. Screenshot rendered with `--demo`.\u003c/sub\u003e\n\n\u003c/div\u003e\n\n---\n\n```text\n🏹 Kamandar  @you  —  2026-06-22 09:14  (business days)  [global]\n========================================================================\n\n📥 Reviews you owe (2)\n----------------------\n  #482 Tighten retry backoff  (acme/api)\n    https://github.com/acme/api/pull/482\n\n  #8   Cache token introspection  (acme/web)\n    https://github.com/acme/web/pull/8\n\n🔨 Currently building (WIP) (1)\n-------------------------------\n  #503 Spike: pluggable providers  (acme/api)\n    https://github.com/acme/api/pull/503\n\n⏳ Your PRs gone quiet (1)\n--------------------------\n  #501 Add billing webhook  (acme/api)  — 3 business days since you handed off\n    https://github.com/acme/api/pull/501\n```\n\n\u003e On a terminal this is colored (a 256-color palette tuned to stay legible on\n\u003e **both light and dark** backgrounds); `#numbers` align within each bucket and\n\u003e entries are spaced for scanning. Piped or redirected, it's plain text with no\n\u003e ANSI.\n\n---\n\n## ✨ What it shows — seven buckets (+ one bonus)\n\n| # | Bucket | What lands here |\n|---|--------|-----------------|\n| 1 | 📥 **Reviews you owe** | Open PRs where review is requested *from you* |\n| 2 | 🔨 **Currently building (WIP)** | Your own open **draft** PRs |\n| 3 | 📋 **Assigned, not started** | Projects V2 issues assigned to you whose **Status** is in a configurable \"not started\" set |\n| 4 | 👀 **Submitted for review** | Projects V2 issues assigned to you whose **Status** is in a configurable \"in review\" set |\n| 5 | 🧪 **In QA** | Projects V2 issues assigned to you whose **Status** is in a configurable \"QA\" set |\n| 6 | 🚧 **Blocked** | Projects V2 issues assigned to you whose **Status** is in a configurable \"blocked\" set (waiting on a requirement or someone's answer) |\n| 7 | ⏳ **Your PRs gone quiet** | Your **ready** PRs where the ball is on the reviewer past a threshold |\n| ➕ | 🙈 **Ready, no reviewer requested** | *(bonus)* Your ready PRs with nobody asked to review and no reviews yet — silently invisible to everyone |\n\n\u003e **The bucket set depends on [scope](#-scope).** The table above is **project**\n\u003e scope, where buckets #3–6 come from your board's **Status** columns. In\n\u003e **global / org / repo** scope there is no board, so those four are replaced by\n\u003e issue+PR buckets driven by the state of each assigned issue's linked PR:\n\u003e\n\u003e | Bucket | Lands here |\n\u003e |---|---|\n\u003e | 📥 Reviews you owe | `review-requested:@me` (same as project) |\n\u003e | 📋 Assigned, not started | issue assigned to you with **no linked PR** |\n\u003e | 🔨 Assigned, PR in draft | linked PR is a **draft** |\n\u003e | 👀 Assigned, PR in review | linked PR is **ready + has a reviewer** |\n\u003e | 🙈 Assigned, PR ready (no reviewer) | linked PR is ready but **nobody asked** |\n\u003e | ⏳ Your PRs gone quiet | same as project |\n\u003e\n\u003e Issue→PR links use GitHub's **\"Closes #123\"** references.\n\n---\n\n## 🚀 Quick start\n\n\u003e Requires **Ruby 3.2+**. No gems — standard library only.\n\n```sh\ngit clone https://github.com/cdrrazan/Kamandar.git\ncd Kamandar\n\n./install.sh                     # symlink `kamandar` into ~/.local/bin (run from anywhere)\nkamandar --init                  # one-time setup: save \u0026 verify your token + login\nkamandar                         # terminal output (default) — from any directory now\n\nkamandar --serve                 # live web app at http://127.0.0.1:4567\nkamandar --dashboard             # full-screen Matrix TUI (digital-rain splash)\nkamandar --menubar               # SwiftBar/xbar plugin — your queue in the macOS top bar\nkamandar --browser               # render + open a static HTML page\nkamandar -b --watch 60           # live tab, refreshed every 60s\nkamandar --serve --demo          # fake data, no token — for screenshots/trials\nkamandar --serve --no-open       # serve headless (don't auto-open a browser tab)\n```\n\n### Run it persistently (macOS)\n\nKeep the web app always-on — starts at login, restarts if it ever exits:\n\n```sh\nruby lib/kamandar.rb --init      # if you haven't already (saves token + login)\ncp ~/.config/kamandar/config .env   # seed the daemon's env (token + login + PORT)\n./service/install-service.sh     # render + load the launchd LaunchAgent\n# → http://127.0.0.1:4567 (logs: ~/Library/Logs/kamandar.{out,err}.log)\n\n./service/uninstall-service.sh   # stop \u0026 remove the service\n```\n\nThe agent runs `kamandar --serve --no-open --tunnel` (headless) with\n`KAMANDAR_CONFIG` pointed at the repo `.env`. `.env` holds your token, so it's\ngit-ignored — never commit it.\n\nThe Ruby server still binds `127.0.0.1` only; the `--tunnel` child runs\n`cloudflared tunnel run kamandar` and publishes it at the hostname in\n`~/.cloudflared/config.yml` (e.g. `kamandar.byaru.com`). **Put Cloudflare Access\nin front of that hostname** — the page is your live GitHub queue, backed by a\nPAT. Don't want it public? Drop `--tunnel` from the plist's `ProgramArguments`\nand re-run the installer for a localhost-only daemon.\n\n\u003e Prefer not to install? Everything also runs in place as `ruby lib/kamandar.rb …`.\n\n\u003e **`--demo`** fabricates 15–20 plausible rows per bucket and skips the network\n\u003e entirely (no token or login needed) — handy for screenshots, demos, or trying a\n\u003e surface offline. It works with any surface (`--serve`, `--browser`, terminal).\n\n\u003e **`--serve`** is the graphical app: a localhost-only web page with a sidebar +\n\u003e tabbed buckets, in-page scope switching, a refresh button, and optional\n\u003e auto-poll — pure HTML + CSS, no JavaScript. Pure stdlib (`TCPServer`), no gems,\n\u003e bound to `127.0.0.1` only, and — like every surface — the token never reaches\n\u003e the page. Use `--port N` (or `PORT`) to change the port.\n\n\u003e `PROJECT_URL` is **optional** — the [scope picker](#-scope) asks for the board\n\u003e URL when you choose `project`. Set it only if you want bucket #3\n\u003e (*Assigned, not started*) populated without picking project scope, or for\n\u003e non-interactive runs (cron).\n\n### Setup without `--init`\n\n`--init` is just a convenience — it writes a config file so you don't re-export\nvars every shell. You can skip it and provide config three ways (highest wins):\n\n1. **CLI flags** — e.g. `--scope`, `--port`, `--theme`.\n2. **Shell env** — `export GITHUB_TOKEN=… GH_LOGIN=…` (best for cron).\n3. **Config file** — a flat `KEY=VALUE` file at `~/.config/kamandar/config`\n   (or `$XDG_CONFIG_HOME/kamandar/config`; `$KAMANDAR_CONFIG` overrides the path\n   entirely). Same names as the env vars:\n\n   ```ini\n   GITHUB_TOKEN=ghp_xxx\n   GH_LOGIN=your-username\n   PROJECT_URL=https://github.com/orgs/Acme/projects/4\n   STALE_DAYS=3\n   ```\n\n   `kamandar --init` writes exactly this file (mode `0600`) after verifying the\n   token against GitHub. Edit it by hand any time.\n\n### Manual install\n\n`install.sh` symlinks `lib/kamandar.rb` (a symlink, so `git pull` updates the\ncommand in place). To do it yourself, or to pick a different directory:\n\n```sh\nchmod +x lib/kamandar.rb\nln -s \"$PWD/lib/kamandar.rb\" ~/.local/bin/kamandar\n# or: KAMANDAR_BIN=/usr/local/bin ./install.sh\n```\n\n---\n\n## 📂 Project layout\n\n```text\nKamandar/\n├── lib/\n│   └── kamandar.rb       # engine + all surfaces + local server (single file, stdlib only)\n├── test/\n│   └── test_kamandar.rb  # acceptance tests — zero network, 254 cases\n├── assets/\n│   ├── logo-web.png      # brand mark (inlined into web surfaces; shown in this README)\n│   └── favicon.ico       # served at /favicon.ico by --serve\n├── install.sh            # symlink the CLI onto your PATH (stdlib only)\n├── README.md\n├── CONTRIBUTING.md\n├── SECURITY.md\n├── V2.md                 # multi-provider roadmap (design only)\n└── LICENSE\n```\n\n---\n\n## ⚙️ Configuration\n\n\u003e Precedence: **CLI flags \u003e environment variables \u003e config file**\n\u003e (`~/.config/kamandar/config`, written by `kamandar --init`).\n\n| Var / flag | Required | Default | Purpose |\n|---|:---:|---|---|\n| `GITHUB_TOKEN` | ✅ | — | Classic PAT: `repo`, `read:org`, `read:project`. From env or config file. |\n| `GH_LOGIN` | ✅ | — | Your GitHub username. From env or config file. |\n| `--init` | | — | One-time wizard: prompt, verify token, write `~/.config/kamandar/config` (`0600`) |\n| `OUTPUT` / `--browser`, `-b` | | `terminal` | Surface: `terminal` or `browser`. The flag forces browser and overrides `OUTPUT`. |\n| `WATCH_SECONDS` / `--watch N` | | `0` (off) | Browser only: re-fetch + rewrite the page every N seconds |\n| `PROJECT_URL` | for #3 | — | Board/view URL, e.g. `https://github.com/orgs/Recognize/projects/10/views/5` |\n| `SCOPE` / `--scope` | | `global` | Scope for PR buckets (#1, #2, #7, bonus). One of `global`, `org[:NAME]`, `repo:owner/name`, `project`. See [Scope](#-scope). |\n| `NOT_STARTED_STATUSES` | | `Todo,Backlog,No Status` | Status names treated as \"not started\" (case-insensitive) — bucket #3 |\n| `REVIEW_STATUSES` | | `In Review,Review,Needs Review` | Status names treated as \"in review\" (case-insensitive) — bucket #4 |\n| `QA_STATUSES` | | `Ready for QA,QA,In QA` | Status names treated as \"in QA\" (case-insensitive) — bucket #5 |\n| `BLOCKED_STATUSES` | | `Blocked,On Hold,Waiting` | Status names treated as \"blocked\" (case-insensitive) — bucket #6 |\n| `ITERATION_FILTER` | | `off` | `current` restricts #3 to the active sprint |\n| `ITERATION_FIELD` | | `Iteration` | Board's iteration field name |\n| `STALE_DAYS` | | `2` | Threshold (in days) for bucket #7 |\n| `DAY_MODE` | | `business` | `business` (skip Sat/Sun) or `calendar` |\n| `THEME` / `--theme` | | — | `matrix` renders a green-on-black boxed TUI (terminal only; pipes stay plain) |\n| `--dashboard` | | off | Full-screen Matrix TUI: digital-rain splash, then live panels (`r` refresh, `q` quit). Needs an interactive TTY; falls back to plain output otherwise |\n| `--serve` | | off | Live web app: localhost-only HTTP server with in-page scope controls + refresh. Token never reaches the page |\n| `--port N` / `PORT` | | `4567` | Port for `--serve` (bound to `127.0.0.1` only) |\n| `--tunnel [name]` / `KAMANDAR_TUNNEL` | | off | Spawn a [Cloudflare Tunnel](#remote-access-via-cloudflare-tunnel) child alongside `--serve` (implies `--serve`); tunnel name defaults to `kamandar`. Needs `cloudflared` on `PATH` |\n| `--demo` | | off | Render fabricated data (15–20 rows/bucket) with no network or token — for screenshots and offline trials |\n\nOnly the **org** and **project number** are parsed from `PROJECT_URL` (via\n`/orgs/\u003corg\u003e/projects/\u003cnum\u003e`); the saved-view number is ignored — see\n[Non-goals](#-non-goals--known-limitations).\n\n\u003e **Finding your board's labels.** The `*_STATUSES` vars (`NOT_STARTED_STATUSES`,\n\u003e `REVIEW_STATUSES`, `QA_STATUSES`, `BLOCKED_STATUSES`) must match your board's\n\u003e actual **Status** column names — a board's columns *are* its Status options.\n\u003e Run `ruby lib/kamandar.rb --statuses` to print every issue assigned to you with\n\u003e its exact Status (and the distinct set), then set the vars to suit. It asks for\n\u003e the board URL if `PROJECT_URL` isn't set.\n\n---\n\n## 🎯 Scope\n\nBy default Kamandar shows your PR buckets **account-wide**. Narrow them with\n`SCOPE` (env) or `--scope` (flag; the flag wins):\n\n| `SCOPE` | What PR buckets (#1, #2, #7, bonus) show |\n|---|---|\n| `global` *(default)* | Every repo your account touches |\n| `org` or `org:NAME` | One org. Bare `org` reuses the org from `PROJECT_URL` |\n| `repo:owner/name` | A single repo |\n| `project` | PRs that **belong to** the `PROJECT_URL` board — a card on it, or one that **closes a board issue** |\n\n```sh\nruby lib/kamandar.rb --scope org:Recognize     # one org\nruby lib/kamandar.rb --scope repo:acme/api     # one repo\nSCOPE=project ruby lib/kamandar.rb             # repos on your project board\n```\n\n`org`/`repo` filter server-side via a GitHub search qualifier; `project` keeps\nonly the PRs that **belong to the board** — either carded on it directly, or\n(the usual case, since boards track issues) a PR that **closes a board issue**\nvia `Closes #N`. Because the board tracks issues, a review you owe is shown as\nthe **board issue** the PR closes (falling back to the PR itself when it closes\nno board issue) — so John's review surfaces as his card in \"Ready for Review\",\nnot a loose PR. Anything unrecognized (or\n`org`/`repo` with no value, or `project` with no `PROJECT_URL`) safely falls\nback to `global`. The active scope is shown in the terminal header and the\nbrowser page.\n\n**Interactive picker.** Run plain `ruby lib/kamandar.rb` in a terminal without\n`SCOPE`/`--scope` and it asks you to pick a mode by number — you only type the\n*name* for `org`/`repo`; you never type the mode itself:\n\n```text\n🏹 Kamandar — which GitHub work should I show?\nPick how wide to look. Press Enter to keep the default.\n\n  1  global   Every repo your account touches      · default\n  2  org      A single organization                · e.g. Recognize\n  3  repo     A single repository                  · e.g. acme/api\n  4  project  A GitHub project board               · paste its URL\n\nChoose 1–4 (Enter = global):\n```\n\nPick `org`/`repo` and it asks for the name (with the expected format and an\nexample); pick `project` and — if no `PROJECT_URL` is set — it asks for the\nboard URL right there (no need to export anything first). A bad `owner/name` or\nboard URL re-prompts; press Enter, or give a blank value, and it defaults to\n**global**. On an interactive terminal the prompt is colored; piped, it's plain.\nThe prompt is skipped when a scope is already set, when stdin isn't a terminal\n(cron/pipes), in `--serve` (the web app picks scope in-page), or in browser\nmode — so nothing ever blocks.\n\n---\n\n## 🏗️ Architecture\n\n**Engine → buckets → Surface** — three separable layers. The engine is pure and\nside-effect-free; surfaces only consume the buckets hash and never re-query or\nre-classify.\n\n```mermaid\nflowchart LR\n    GH[\"GitHub GraphQL API\"] --\u003e|\"1 aliased call + paginated board\"| FETCH[\"Fetch layer\"]\n    FETCH --\u003e ENGINE[\"Engine (pure)\u003cbr/\u003etime math · classification\"]\n    ENGINE --\u003e BUCKETS[\"Buckets\u003cbr/\u003e(plain hash)\"]\n    BUCKETS --\u003e TERM[\"🖥️ Terminal\u003cbr/\u003eplain/color · cron-friendly\"]\n    BUCKETS --\u003e DASH[\"🟩 Dashboard\u003cbr/\u003efull-screen Matrix TUI\"]\n    BUCKETS --\u003e BROWSER[\"🌐 Browser\u003cbr/\u003estatic offline HTML\"]\n    BUCKETS --\u003e SERVE[\"🔌 Server (--serve)\u003cbr/\u003elocalhost web app\"]\n    BUCKETS --\u003e MENUBAR[\"🏹 Menu bar (--menubar)\u003cbr/\u003eSwiftBar/xbar plugin\"]\n```\n\n- **Engine** — pure functions (GraphQL building, time math, classification),\n  unit-testable with zero network.\n- **Buckets** — a plain hash the engine returns. The set depends on scope\n  (board-driven for `project`, issue+PR-driven otherwise).\n- **Surface** — one tiny contract (`render`/`page(buckets, ...) -\u003e String` + an\n  `emit`). Five implementations today: terminal, dashboard, browser, the\n  `--serve` web app (which reuses the browser surface's CSS/cards), and the\n  `--menubar` SwiftBar/xbar plugin. Each was added with **no engine change** —\n  email would be the same.\n- **Server** — the only *inbound* network layer: a minimal stdlib `TCPServer`\n  HTTP/1.1 loop for `--serve`, bound to `127.0.0.1`. Pure helpers (request\n  parsing, response framing, scope resolution) are unit-tested; the accept loop\n  lives in the CLI.\n\nEverything is guarded by `if __FILE__ == $PROGRAM_NAME` so the test suite can\n`require` the file with zero network and no ENV reads.\n\n---\n\n## 🖥️ Surfaces\n\nThe same classified buckets feed every surface — no surface re-queries or\nre-classifies.\n\n| Surface | Command | Best for | Network |\n|---|---|:---:|---|\n| 🖥️ **Terminal** | `kamandar` | cron, pipes, quick checks | outbound only |\n| 🟩 **Dashboard** | `kamandar --dashboard` | an ambient TTY status board | outbound only |\n| 🌐 **Browser** | `kamandar --browser` | an offline, shareable HTML snapshot | none (static file) |\n| 🔌 **Live web app** | `kamandar --serve` | an interactive, app-like UI | localhost listener |\n| 🏹 **Menu bar** | `kamandar --menubar` | an always-there macOS top-bar count | outbound only |\n\n### Terminal (default)\n\nGrouped by bucket with per-bucket emoji and color **when stdout is a terminal**.\nColors use a 256-color palette tuned to stay readable on **both light and dark**\nbackgrounds; `#numbers` are aligned per bucket and entries are spaced for\nscanning. Piped or redirected (cron, `| mail`), it automatically falls back to\nplain text with no ANSI — so captured output stays clean.\n\nPrefer a retro look? `THEME=matrix ruby lib/kamandar.rb` (or `--theme matrix`)\ndraws a green-on-black boxed dashboard. It's TTY-only — piped output is still\nplain text.\n\n### Dashboard (`--dashboard`)\n\nA full-screen **Matrix TUI**: a digital-rain splash, then live green panels of\nevery bucket. Keys: `r` refetches, `q` (or Ctrl-C) quits. It takes over the\nalt-screen buffer and always restores it on exit. Needs an interactive TTY\n(stdout **and** stdin) — pipes/cron fall back to plain output with a notice.\n\n### Live web app (`--serve`)\n\nA **localhost-only** web page served by a minimal stdlib `TCPServer` — the\ngraphical, app-like surface. It re-fetches and re-classifies server-side per\nrequest; bound to `127.0.0.1` only, `--port N` (or `PORT`) to change the port,\nand — like every surface — the token never reaches any response. A fetch blip\nrenders an error page instead of dropping the server.\n\n```sh\nruby lib/kamandar.rb --serve            # http://127.0.0.1:4567\nruby lib/kamandar.rb --serve --port 8080\nruby lib/kamandar.rb --serve --demo     # fabricated data, no token (the screenshot above)\n```\n\nThe page is a small \"command center\" UI, and it's **pure HTML + CSS — no\nJavaScript**:\n\n- **Sticky app header** — a frosted top nav (the **bow-and-arrow logo** +\n  wordmark, status chips, GitHub repo link) over a **toolbar row** with the\n  scope control. The scope picker is a\n  segmented control; the page shows just what each scope needs — `global` is\n  only the picker + **Apply** + **refresh**, while `org`/`repo` reveal the name\n  field, `project` reveals the board URL, and any non-global scope reveals the\n  **auto-refresh** interval. The reveal is driven by CSS `:has()`, no scripting.\n- **Sidebar + tabs** — buckets become tabs in two cards: **Others' work**\n  (reviews requested from you) and **Your work** (your assigned issues/PRs).\n  Each card shows its own open count; the selected tab fills with the bucket's\n  color. Tab switching is pure-CSS (a hidden radio per bucket).\n- **Main panel** — the selected bucket, with a one-line description of what it\n  collects under the heading, the matching cards, or a centered empty-state\n  card when nothing's waiting. Buckets with more than 8 cards **paginate**, with\n  a numbered pager (also pure CSS — a hidden radio per page).\n- **Footer** — version, the localhost/stdlib note, repo link, and the\n  generated-at time.\n- **Branding** — the logo is **inlined as a data URI** (so it ships inside the\n  HTML, no extra request), and the server answers `GET /favicon.ico` with\n  `assets/favicon.ico` so the browser tab gets the bow-and-arrow icon.\n- Loads the **Google Sans** webfont (the one network asset, allowed here since\n  the served page is online), falling back to the system font stack. Tracks\n  light/dark via `prefers-color-scheme`.\n\n#### Remote access via Cloudflare Tunnel\n\nWant to reach the app from your phone or another machine at a real URL —\n`https://kamandar.yourdomain.com` instead of `localhost:4567`? A\n[Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/)\ndoes it **without changing the bind**. `cloudflared` runs on the same machine as\n`--serve` and dials *outbound* to `127.0.0.1:4567`, so the server stays\nlocalhost-only — no `0.0.0.0`, no port-forward, no open inbound firewall ports.\n\n```\n  phone / laptop ──HTTPS──► Cloudflare edge ──tunnel──► cloudflared ──► 127.0.0.1:4567\n                          (kamandar.yourdomain.com)        (your machine)        (--serve)\n```\n\n\u003e 🔒 **Gate it with Cloudflare Access — this is not optional.** The page shows\n\u003e your **private** GitHub queue (repos, PR/issue titles). The token never\n\u003e reaches the HTML, but the *data* is sensitive, and a tunnel hostname is\n\u003e reachable by anyone who has it. A self-hosted **Access** policy (free on\n\u003e Cloudflare Zero Trust) puts an email/SSO login in front so only *you* get in.\n\u003e Without it, your work queue is on the public internet.\n\n**One-time setup** (needs a Cloudflare account + your domain on Cloudflare, and\n[`cloudflared` installed](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/)):\n\n```sh\ncloudflared tunnel login                                   # authorize, pick your domain\ncloudflared tunnel create kamandar                         # creates the tunnel + a creds file\ncloudflared tunnel route dns kamandar kamandar.yourdomain.com   # point the hostname at it\n```\n\nThen map the hostname to the local port in `~/.cloudflared/config.yml`:\n\n```yaml\ntunnel: kamandar\ningress:\n  - hostname: kamandar.yourdomain.com\n    service: http://127.0.0.1:4567\n  - service: http_status:404\n```\n\nFinally, in the **Cloudflare Zero Trust dashboard** → *Access → Applications*,\nadd a **self-hosted** app for `kamandar.yourdomain.com` with a policy that\nallows only your email. (Do this before sharing or bookmarking the URL.)\n\n**Daily use** — one command brings up both halves. `--tunnel` spawns\n`cloudflared` as a child of `--serve` and tears it down on exit, so a single\nCtrl-C stops the server *and* the tunnel:\n\n```sh\nkamandar --serve --tunnel        # app on 127.0.0.1:4567 + the Cloudflare Tunnel\n# → open https://kamandar.yourdomain.com, log in via Access, set Auto-refresh ~30s\n# → Ctrl-C once; both the server and the tunnel shut down. URL goes dead until next time.\n```\n\n`--tunnel` runs the tunnel named `kamandar` by default; override with\n`--tunnel \u003cname\u003e` or `$KAMANDAR_TUNNEL`. It reads `~/.cloudflared/config.yml`\nfor the hostname mapping (above), and falls back to serving locally (with a\nwarning) if `cloudflared` isn't on your `PATH`. `--tunnel` implies `--serve`.\n\n\u003cdetails\u003e\u003csummary\u003ePrefer two terminals?\u003c/summary\u003e\n\n```sh\nkamandar --serve                 # terminal 1: the app on 127.0.0.1:4567\ncloudflared tunnel run kamandar  # terminal 2: bridge to kamandar.yourdomain.com\n```\n\u003c/details\u003e\n\nNotes:\n\n- **`--serve` is the host.** The tunnel is only a pipe — kill `--serve` and the\n  URL returns 502. Both processes must run on the same machine, and that machine\n  must stay awake (laptop asleep = URL down).\n- The token + login live **only on that machine** (via `--init` or env); they\n  never travel through Cloudflare.\n- Auto-refresh re-queries GitHub each cycle, so keep `poll` sane (`30`+, not\n  `5`) to stay well under API rate limits.\n- For a quick throwaway demo with **no domain and no setup**, swap the named\n  tunnel for `cloudflared tunnel --url http://127.0.0.1:4567` — it prints a\n  random `*.trycloudflare.com` URL. That one is **unauthenticated**; treat the\n  link as public and use it only for fabricated `--demo` data.\n\n### Browser (offline file)\n\nRenders **one self-contained HTML document** (inline CSS, no external/CDN\nresources, works offline over `file://`) to a stable path\n(`\u003ctmpdir\u003e/kamandar.html`) and opens it in your default browser. Bucket #7\ngets a warning accent and a \"days since handoff\" badge per card. Dark mode via\n`prefers-color-scheme`.\n\n- **Watch mode** (`--watch N`): re-fetches, re-classifies, and rewrites the same\n  file every N seconds — opening the browser only on the first cycle. The page\n  carries `\u003cmeta http-equiv=\"refresh\"\u003e` so the open tab reloads itself.\n  Meta-refresh over `file://` works in current Chrome, Firefox, and Safari.\n- 🔒 **Security:** the page is a static in-process snapshot. It makes no GitHub\n  calls and **never contains your token or any secret** — see\n  [SECURITY.md](SECURITY.md).\n\n### Menu bar (`--menubar`)\n\nYour queue **always in the macOS top bar** — a 🏹 + open count, click for a\ndropdown of every bucket with each PR/issue deep-linked to GitHub. The bar tints\n**orange** when a review is owed or a PR has gone quiet.\n\nstdlib Ruby can't draw a status item, so this rides on\n[**SwiftBar**](https://swiftbar.app) (or [xbar](https://xbarapp.com)) — a tiny\napp that runs a script on an interval and renders its stdout in the menu bar.\n`--menubar` emits exactly that plugin document (one line = the bar title, `---`\nopens the dropdown, `--` nests a row, ` | href= color=` set params). SwiftBar is\nan external app, like `cloudflared` — **not** a Ruby dependency.\n\n```sh\nbrew install swiftbar                 # then launch it once and pick a plugin folder\n./menubar/install-menubar.sh 5m       # render the plugin into that folder, refresh every 5m\n./menubar/uninstall-menubar.sh        # remove it\n```\n\nThe installer fills in this machine's repo/ruby/home and points\n`KAMANDAR_CONFIG` at the repo `.env` (token + login), so the plugin loads\ncredentials the same way the launchd service does. Long buckets cap at 12 rows\nwith an \"…and N more\" link to the web app. Like every surface, **the token never\nappears in the output**.\n\n```sh\nkamandar --menubar          # print the plugin document yourself (one shot)\nkamandar --menubar --demo   # fabricated data, no token — preview the layout\n```\n\n---\n\n## ⏳ Bucket #7 — the handoff-vs-reviewer race\n\nKeying off `reviewDecision == REVIEW_REQUIRED` is **wrong**: after a reviewer\nrequests changes and the author pushes fixes, `reviewDecision` stays\n`CHANGES_REQUESTED` until the reviewer re-reviews — so the PR you most want\nflagged gets dropped. kamandar uses a **timestamp race** instead.\n\n```mermaid\nflowchart TD\n    A[\"handoff = max(last review-requested, last push, PR created)\"] --\u003e C{ball on reviewer?}\n    B[\"reviewer action = latest APPROVED / CHANGES_REQUESTED\u003cbr/\u003e(plain comments ignored)\"] --\u003e C\n    C --\u003e|\"handoff \u0026gt; action, or never acted\"| D{\"days since handoff ≥ STALE_DAYS\u003cbr/\u003eand not a draft?\"}\n    C --\u003e|\"action newer\"| E[\"not stale (ball on author)\"]\n    D --\u003e|yes| F[\"⏳ STALE\"]\n    D --\u003e|no| G[\"not stale yet\"]\n```\n\n| Scenario | Result |\n|---|---|\n| Fresh, awaiting review | ⏳ stale |\n| Changes requested, not yet fixed | ✅ not stale (ball on author) |\n| Changes requested, **then pushed** | ⏳ stale |\n| Approved, no new commits | ✅ not stale |\n| Approved, **then pushed** | ⏳ stale |\n| No reviewer at all | 🙈 forgot-reviewer (not stale) |\n\n---\n\n## 📨 Push layer (terminal mode)\n\nNo scheduler code lives in the tool. Wire terminal output into your own cron —\ne.g. weekday mornings at 8:30, emailed to yourself:\n\n```cron\n30 8 * * 1-5  GITHUB_TOKEN=... GH_LOGIN=you PROJECT_URL=... \\\n              ruby /path/lib/kamandar.rb | mail -s \"Kamandar\" you@example.com\n```\n\nSwap `mail` for `notify-send` (Linux desktop) or `terminal-notifier` (macOS).\nBrowser mode is for interactive/ambient use (optionally with `--watch`), not cron.\n\n---\n\n## ✅ Tests\n\nEvery acceptance scenario is encoded with a fixed \"today\" (Monday 2026-06-22)\nand fabricated fixtures — **zero network**.\n\n```sh\nruby test/test_kamandar.rb\n# ...\n# 254 passed, 0 failed\n```\n\n---\n\n## 🗺️ Roadmap\n\nA **v2** that abstracts the provider layer to support GitLab and other project\nmanagers (Jira, Linear) is sketched in [V2.md](V2.md).\n\n---\n\n## 🚫 Non-goals / known limitations\n\n- The saved **view** filter DSL is **not** replicated; #3 is approximated by\n  Status (+ optional iteration). Only org + project number are read from the URL.\n- \"Commented\" reviews are intentionally ignored — a comment doesn't flip the ball.\n- Any push (incl. a typo fix or rebase/force-push) resets the #4 clock by design\n  (\"you resubmitted\"). To reset only on an explicit re-request, drop `last_push`\n  from `handoff_at` in the engine.\n- Browser mode is a **static snapshot** rendered in-process: no client-side\n  GitHub calls, no live data except via `--watch` re-runs. The token never\n  reaches the page.\n- `--serve` is a **single-user, localhost-only** convenience: plain HTTP bound\n  to `127.0.0.1`, no auth, one request at a time. Don't expose it to a network\n  or proxy it to a public address — see [SECURITY.md](SECURITY.md).\n- Single user, single token, no multi-tenant concerns.\n\n---\n\n## 🤝 Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md). Security policy in [SECURITY.md](SECURITY.md).\n\n## 📄 License\n\n[MIT](LICENSE) © 2026 cdrrazan\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcdrrazan%2Fkamandar","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcdrrazan%2Fkamandar","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcdrrazan%2Fkamandar/lists"}