Kamandar (Ϊ©Ω
Ψ§ΩβΨ―Ψ§Ψ±) is Persian for archer β one who draws the bow and
finds the target. A personal GitHub command center: one command shows what you
owe, what you're building, what's assigned, and what's gone quiet β as a
colored terminal report, a full-screen Matrix dashboard, a self-contained
browser page, or a live local web app. No backend; the only network listener is
the opt-in --serve, bound to localhost.
The live web app (ruby lib/kamandar.rb --serve). Pure HTML + CSS β sidebar tabs, two work boxes, and pagination, no JavaScript. Screenshot rendered with --demo.
πΉ Kamandar @you β 2026-06-22 09:14 (business days) [global]
========================================================================
π₯ Reviews you owe (2)
----------------------
#482 Tighten retry backoff (acme/api)
https://github.com/acme/api/pull/482
#8 Cache token introspection (acme/web)
https://github.com/acme/web/pull/8
π¨ Currently building (WIP) (1)
-------------------------------
#503 Spike: pluggable providers (acme/api)
https://github.com/acme/api/pull/503
β³ Your PRs gone quiet (1)
--------------------------
#501 Add billing webhook (acme/api) β 3 business days since you handed off
https://github.com/acme/api/pull/501
On a terminal this is colored (a 256-color palette tuned to stay legible on both light and dark backgrounds);
#numbersalign within each bucket and entries are spaced for scanning. Piped or redirected, it's plain text with no ANSI.
| # | Bucket | What lands here |
|---|---|---|
| 1 | π₯ Reviews you owe | Open PRs where review is requested from you |
| 2 | π¨ Currently building (WIP) | Your own open draft PRs |
| 3 | π Assigned, not started | Projects V2 issues assigned to you whose Status is in a configurable "not started" set |
| 4 | π Submitted for review | Projects V2 issues assigned to you whose Status is in a configurable "in review" set |
| 5 | π§ͺ In QA | Projects V2 issues assigned to you whose Status is in a configurable "QA" set |
| 6 | π§ Blocked | Projects V2 issues assigned to you whose Status is in a configurable "blocked" set (waiting on a requirement or someone's answer) |
| 7 | β³ Your PRs gone quiet | Your ready PRs where the ball is on the reviewer past a threshold |
| β | π Ready, no reviewer requested | (bonus) Your ready PRs with nobody asked to review and no reviews yet β silently invisible to everyone |
The bucket set depends on scope. The table above is project scope, where buckets #3β6 come from your board's Status columns. In global / org / repo scope there is no board, so those four are replaced by issue+PR buckets driven by the state of each assigned issue's linked PR:
Bucket Lands here π₯ Reviews you owe review-requested:@me(same as project)π Assigned, not started issue assigned to you with no linked PR π¨ Assigned, PR in draft linked PR is a draft π Assigned, PR in review linked PR is ready + has a reviewer π Assigned, PR ready (no reviewer) linked PR is ready but nobody asked β³ Your PRs gone quiet same as project IssueβPR links use GitHub's "Closes #123" references.
Requires Ruby 3.2+. No gems β standard library only.
git clone https://github.com/cdrrazan/Kamandar.git
cd Kamandar
./install.sh # symlink `kamandar` into ~/.local/bin (run from anywhere)
kamandar --init # one-time setup: save & verify your token + login
kamandar # terminal output (default) β from any directory now
kamandar --serve # live web app at http://127.0.0.1:4567
kamandar --dashboard # full-screen Matrix TUI (digital-rain splash)
kamandar --menubar # SwiftBar/xbar plugin β your queue in the macOS top bar
kamandar --browser # render + open a static HTML page
kamandar -b --watch 60 # live tab, refreshed every 60s
kamandar --serve --demo # fake data, no token β for screenshots/trials
kamandar --serve --no-open # serve headless (don't auto-open a browser tab)Keep the web app always-on β starts at login, restarts if it ever exits:
ruby lib/kamandar.rb --init # if you haven't already (saves token + login)
cp ~/.config/kamandar/config .env # seed the daemon's env (token + login + PORT)
./service/install-service.sh # render + load the launchd LaunchAgent
# β http://127.0.0.1:4567 (logs: ~/Library/Logs/kamandar.{out,err}.log)
./service/uninstall-service.sh # stop & remove the serviceThe agent runs kamandar --serve --no-open --tunnel (headless) with
KAMANDAR_CONFIG pointed at the repo .env. .env holds your token, so it's
git-ignored β never commit it.
The Ruby server still binds 127.0.0.1 only; the --tunnel child runs
cloudflared tunnel run kamandar and publishes it at the hostname in
~/.cloudflared/config.yml (e.g. kamandar.byaru.com). Put Cloudflare Access
in front of that hostname β the page is your live GitHub queue, backed by a
PAT. Don't want it public? Drop --tunnel from the plist's ProgramArguments
and re-run the installer for a localhost-only daemon.
Prefer not to install? Everything also runs in place as
ruby lib/kamandar.rb β¦.
--demofabricates 15β20 plausible rows per bucket and skips the network entirely (no token or login needed) β handy for screenshots, demos, or trying a surface offline. It works with any surface (--serve,--browser, terminal).
--serveis the graphical app: a localhost-only web page with a sidebar + tabbed buckets, in-page scope switching, a refresh button, and optional auto-poll β pure HTML + CSS, no JavaScript. Pure stdlib (TCPServer), no gems, bound to127.0.0.1only, and β like every surface β the token never reaches the page. Use--port N(orPORT) to change the port.
PROJECT_URLis optional β the scope picker asks for the board URL when you chooseproject. Set it only if you want bucket #3 (Assigned, not started) populated without picking project scope, or for non-interactive runs (cron).
--init is just a convenience β it writes a config file so you don't re-export
vars every shell. You can skip it and provide config three ways (highest wins):
-
CLI flags β e.g.
--scope,--port,--theme. -
Shell env β
export GITHUB_TOKEN=β¦ GH_LOGIN=β¦(best for cron). -
Config file β a flat
KEY=VALUEfile at~/.config/kamandar/config(or$XDG_CONFIG_HOME/kamandar/config;$KAMANDAR_CONFIGoverrides the path entirely). Same names as the env vars:GITHUB_TOKEN=ghp_xxx GH_LOGIN=your-username PROJECT_URL=https://github.com/orgs/Acme/projects/4 STALE_DAYS=3
kamandar --initwrites exactly this file (mode0600) after verifying the token against GitHub. Edit it by hand any time.
install.sh symlinks lib/kamandar.rb (a symlink, so git pull updates the
command in place). To do it yourself, or to pick a different directory:
chmod +x lib/kamandar.rb
ln -s "$PWD/lib/kamandar.rb" ~/.local/bin/kamandar
# or: KAMANDAR_BIN=/usr/local/bin ./install.shKamandar/
βββ lib/
β βββ kamandar.rb # engine + all surfaces + local server (single file, stdlib only)
βββ test/
β βββ test_kamandar.rb # acceptance tests β zero network, 254 cases
βββ assets/
β βββ logo-web.png # brand mark (inlined into web surfaces; shown in this README)
β βββ favicon.ico # served at /favicon.ico by --serve
βββ install.sh # symlink the CLI onto your PATH (stdlib only)
βββ README.md
βββ CONTRIBUTING.md
βββ SECURITY.md
βββ V2.md # multi-provider roadmap (design only)
βββ LICENSE
Precedence: CLI flags > environment variables > config file (
~/.config/kamandar/config, written bykamandar --init).
| Var / flag | Required | Default | Purpose |
|---|---|---|---|
GITHUB_TOKEN |
β | β | Classic PAT: repo, read:org, read:project. From env or config file. |
GH_LOGIN |
β | β | Your GitHub username. From env or config file. |
--init |
β | One-time wizard: prompt, verify token, write ~/.config/kamandar/config (0600) |
|
OUTPUT / --browser, -b |
terminal |
Surface: terminal or browser. The flag forces browser and overrides OUTPUT. |
|
WATCH_SECONDS / --watch N |
0 (off) |
Browser only: re-fetch + rewrite the page every N seconds | |
PROJECT_URL |
for #3 | β | Board/view URL, e.g. https://github.com/orgs/Recognize/projects/10/views/5 |
SCOPE / --scope |
global |
Scope for PR buckets (#1, #2, #7, bonus). One of global, org[:NAME], repo:owner/name, project. See Scope. |
|
NOT_STARTED_STATUSES |
Todo,Backlog,No Status |
Status names treated as "not started" (case-insensitive) β bucket #3 | |
REVIEW_STATUSES |
In Review,Review,Needs Review |
Status names treated as "in review" (case-insensitive) β bucket #4 | |
QA_STATUSES |
Ready for QA,QA,In QA |
Status names treated as "in QA" (case-insensitive) β bucket #5 | |
BLOCKED_STATUSES |
Blocked,On Hold,Waiting |
Status names treated as "blocked" (case-insensitive) β bucket #6 | |
ITERATION_FILTER |
off |
current restricts #3 to the active sprint |
|
ITERATION_FIELD |
Iteration |
Board's iteration field name | |
STALE_DAYS |
2 |
Threshold (in days) for bucket #7 | |
DAY_MODE |
business |
business (skip Sat/Sun) or calendar |
|
THEME / --theme |
β | matrix renders a green-on-black boxed TUI (terminal only; pipes stay plain) |
|
--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 |
|
--serve |
off | Live web app: localhost-only HTTP server with in-page scope controls + refresh. Token never reaches the page | |
--port N / PORT |
4567 |
Port for --serve (bound to 127.0.0.1 only) |
|
--tunnel [name] / KAMANDAR_TUNNEL |
off | Spawn a Cloudflare Tunnel child alongside --serve (implies --serve); tunnel name defaults to kamandar. Needs cloudflared on PATH |
|
--demo |
off | Render fabricated data (15β20 rows/bucket) with no network or token β for screenshots and offline trials |
Only the org and project number are parsed from PROJECT_URL (via
/orgs/<org>/projects/<num>); the saved-view number is ignored β see
Non-goals.
Finding your board's labels. The
*_STATUSESvars (NOT_STARTED_STATUSES,REVIEW_STATUSES,QA_STATUSES,BLOCKED_STATUSES) must match your board's actual Status column names β a board's columns are its Status options. Runruby lib/kamandar.rb --statusesto print every issue assigned to you with its exact Status (and the distinct set), then set the vars to suit. It asks for the board URL ifPROJECT_URLisn't set.
By default Kamandar shows your PR buckets account-wide. Narrow them with
SCOPE (env) or --scope (flag; the flag wins):
SCOPE |
What PR buckets (#1, #2, #7, bonus) show |
|---|---|
global (default) |
Every repo your account touches |
org or org:NAME |
One org. Bare org reuses the org from PROJECT_URL |
repo:owner/name |
A single repo |
project |
PRs that belong to the PROJECT_URL board β a card on it, or one that closes a board issue |
ruby lib/kamandar.rb --scope org:Recognize # one org
ruby lib/kamandar.rb --scope repo:acme/api # one repo
SCOPE=project ruby lib/kamandar.rb # repos on your project boardorg/repo filter server-side via a GitHub search qualifier; project keeps
only the PRs that belong to the board β either carded on it directly, or
(the usual case, since boards track issues) a PR that closes a board issue
via Closes #N. Because the board tracks issues, a review you owe is shown as
the board issue the PR closes (falling back to the PR itself when it closes
no board issue) β so John's review surfaces as his card in "Ready for Review",
not a loose PR. Anything unrecognized (or
org/repo with no value, or project with no PROJECT_URL) safely falls
back to global. The active scope is shown in the terminal header and the
browser page.
Interactive picker. Run plain ruby lib/kamandar.rb in a terminal without
SCOPE/--scope and it asks you to pick a mode by number β you only type the
name for org/repo; you never type the mode itself:
πΉ Kamandar β which GitHub work should I show?
Pick how wide to look. Press Enter to keep the default.
1 global Every repo your account touches Β· default
2 org A single organization Β· e.g. Recognize
3 repo A single repository Β· e.g. acme/api
4 project A GitHub project board Β· paste its URL
Choose 1β4 (Enter = global):
Pick org/repo and it asks for the name (with the expected format and an
example); pick project and β if no PROJECT_URL is set β it asks for the
board URL right there (no need to export anything first). A bad owner/name or
board URL re-prompts; press Enter, or give a blank value, and it defaults to
global. On an interactive terminal the prompt is colored; piped, it's plain.
The prompt is skipped when a scope is already set, when stdin isn't a terminal
(cron/pipes), in --serve (the web app picks scope in-page), or in browser
mode β so nothing ever blocks.
Engine β buckets β Surface β three separable layers. The engine is pure and side-effect-free; surfaces only consume the buckets hash and never re-query or re-classify.
flowchart LR
GH["GitHub GraphQL API"] -->|"1 aliased call + paginated board"| FETCH["Fetch layer"]
FETCH --> ENGINE["Engine (pure)<br/>time math Β· classification"]
ENGINE --> BUCKETS["Buckets<br/>(plain hash)"]
BUCKETS --> TERM["π₯οΈ Terminal<br/>plain/color Β· cron-friendly"]
BUCKETS --> DASH["π© Dashboard<br/>full-screen Matrix TUI"]
BUCKETS --> BROWSER["π Browser<br/>static offline HTML"]
BUCKETS --> SERVE["π Server (--serve)<br/>localhost web app"]
BUCKETS --> MENUBAR["πΉ Menu bar (--menubar)<br/>SwiftBar/xbar plugin"]
- Engine β pure functions (GraphQL building, time math, classification), unit-testable with zero network.
- Buckets β a plain hash the engine returns. The set depends on scope
(board-driven for
project, issue+PR-driven otherwise). - Surface β one tiny contract (
render/page(buckets, ...) -> String+ anemit). Five implementations today: terminal, dashboard, browser, the--serveweb app (which reuses the browser surface's CSS/cards), and the--menubarSwiftBar/xbar plugin. Each was added with no engine change β email would be the same. - Server β the only inbound network layer: a minimal stdlib
TCPServerHTTP/1.1 loop for--serve, bound to127.0.0.1. Pure helpers (request parsing, response framing, scope resolution) are unit-tested; the accept loop lives in the CLI.
Everything is guarded by if __FILE__ == $PROGRAM_NAME so the test suite can
require the file with zero network and no ENV reads.
The same classified buckets feed every surface β no surface re-queries or re-classifies.
| Surface | Command | Best for | Network |
|---|---|---|---|
| π₯οΈ Terminal | kamandar |
cron, pipes, quick checks | outbound only |
| π© Dashboard | kamandar --dashboard |
an ambient TTY status board | outbound only |
| π Browser | kamandar --browser |
an offline, shareable HTML snapshot | none (static file) |
| π Live web app | kamandar --serve |
an interactive, app-like UI | localhost listener |
| πΉ Menu bar | kamandar --menubar |
an always-there macOS top-bar count | outbound only |
Grouped by bucket with per-bucket emoji and color when stdout is a terminal.
Colors use a 256-color palette tuned to stay readable on both light and dark
backgrounds; #numbers are aligned per bucket and entries are spaced for
scanning. Piped or redirected (cron, | mail), it automatically falls back to
plain text with no ANSI β so captured output stays clean.
Prefer a retro look? THEME=matrix ruby lib/kamandar.rb (or --theme matrix)
draws a green-on-black boxed dashboard. It's TTY-only β piped output is still
plain text.
A full-screen Matrix TUI: a digital-rain splash, then live green panels of
every bucket. Keys: r refetches, q (or Ctrl-C) quits. It takes over the
alt-screen buffer and always restores it on exit. Needs an interactive TTY
(stdout and stdin) β pipes/cron fall back to plain output with a notice.
A localhost-only web page served by a minimal stdlib TCPServer β the
graphical, app-like surface. It re-fetches and re-classifies server-side per
request; bound to 127.0.0.1 only, --port N (or PORT) to change the port,
and β like every surface β the token never reaches any response. A fetch blip
renders an error page instead of dropping the server.
ruby lib/kamandar.rb --serve # http://127.0.0.1:4567
ruby lib/kamandar.rb --serve --port 8080
ruby lib/kamandar.rb --serve --demo # fabricated data, no token (the screenshot above)The page is a small "command center" UI, and it's pure HTML + CSS β no JavaScript:
- Sticky app header β a frosted top nav (the bow-and-arrow logo +
wordmark, status chips, GitHub repo link) over a toolbar row with the
scope control. The scope picker is a
segmented control; the page shows just what each scope needs β
globalis only the picker + Apply + refresh, whileorg/reporeveal the name field,projectreveals the board URL, and any non-global scope reveals the auto-refresh interval. The reveal is driven by CSS:has(), no scripting. - Sidebar + tabs β buckets become tabs in two cards: Others' work (reviews requested from you) and Your work (your assigned issues/PRs). Each card shows its own open count; the selected tab fills with the bucket's color. Tab switching is pure-CSS (a hidden radio per bucket).
- Main panel β the selected bucket, with a one-line description of what it collects under the heading, the matching cards, or a centered empty-state card when nothing's waiting. Buckets with more than 8 cards paginate, with a numbered pager (also pure CSS β a hidden radio per page).
- Footer β version, the localhost/stdlib note, repo link, and the generated-at time.
- Branding β the logo is inlined as a data URI (so it ships inside the
HTML, no extra request), and the server answers
GET /favicon.icowithassets/favicon.icoso the browser tab gets the bow-and-arrow icon. - Loads the Google Sans webfont (the one network asset, allowed here since
the served page is online), falling back to the system font stack. Tracks
light/dark via
prefers-color-scheme.
Want to reach the app from your phone or another machine at a real URL β
https://kamandar.yourdomain.com instead of localhost:4567? A
Cloudflare Tunnel
does it without changing the bind. cloudflared runs on the same machine as
--serve and dials outbound to 127.0.0.1:4567, so the server stays
localhost-only β no 0.0.0.0, no port-forward, no open inbound firewall ports.
phone / laptop ββHTTPSβββΊ Cloudflare edge ββtunnelβββΊ cloudflared βββΊ 127.0.0.1:4567
(kamandar.yourdomain.com) (your machine) (--serve)
π Gate it with Cloudflare Access β this is not optional. The page shows your private GitHub queue (repos, PR/issue titles). The token never reaches the HTML, but the data is sensitive, and a tunnel hostname is reachable by anyone who has it. A self-hosted Access policy (free on Cloudflare Zero Trust) puts an email/SSO login in front so only you get in. Without it, your work queue is on the public internet.
One-time setup (needs a Cloudflare account + your domain on Cloudflare, and
cloudflared installed):
cloudflared tunnel login # authorize, pick your domain
cloudflared tunnel create kamandar # creates the tunnel + a creds file
cloudflared tunnel route dns kamandar kamandar.yourdomain.com # point the hostname at itThen map the hostname to the local port in ~/.cloudflared/config.yml:
tunnel: kamandar
ingress:
- hostname: kamandar.yourdomain.com
service: http://127.0.0.1:4567
- service: http_status:404Finally, in the Cloudflare Zero Trust dashboard β Access β Applications,
add a self-hosted app for kamandar.yourdomain.com with a policy that
allows only your email. (Do this before sharing or bookmarking the URL.)
Daily use β one command brings up both halves. --tunnel spawns
cloudflared as a child of --serve and tears it down on exit, so a single
Ctrl-C stops the server and the tunnel:
kamandar --serve --tunnel # app on 127.0.0.1:4567 + the Cloudflare Tunnel
# β open https://kamandar.yourdomain.com, log in via Access, set Auto-refresh ~30s
# β Ctrl-C once; both the server and the tunnel shut down. URL goes dead until next time.--tunnel runs the tunnel named kamandar by default; override with
--tunnel <name> or $KAMANDAR_TUNNEL. It reads ~/.cloudflared/config.yml
for the hostname mapping (above), and falls back to serving locally (with a
warning) if cloudflared isn't on your PATH. --tunnel implies --serve.
Prefer two terminals?
kamandar --serve # terminal 1: the app on 127.0.0.1:4567
cloudflared tunnel run kamandar # terminal 2: bridge to kamandar.yourdomain.comNotes:
--serveis the host. The tunnel is only a pipe β kill--serveand the URL returns 502. Both processes must run on the same machine, and that machine must stay awake (laptop asleep = URL down).- The token + login live only on that machine (via
--initor env); they never travel through Cloudflare. - Auto-refresh re-queries GitHub each cycle, so keep
pollsane (30+, not5) to stay well under API rate limits. - For a quick throwaway demo with no domain and no setup, swap the named
tunnel for
cloudflared tunnel --url http://127.0.0.1:4567β it prints a random*.trycloudflare.comURL. That one is unauthenticated; treat the link as public and use it only for fabricated--demodata.
Renders one self-contained HTML document (inline CSS, no external/CDN
resources, works offline over file://) to a stable path
(<tmpdir>/kamandar.html) and opens it in your default browser. Bucket #7
gets a warning accent and a "days since handoff" badge per card. Dark mode via
prefers-color-scheme.
- Watch mode (
--watch N): re-fetches, re-classifies, and rewrites the same file every N seconds β opening the browser only on the first cycle. The page carries<meta http-equiv="refresh">so the open tab reloads itself. Meta-refresh overfile://works in current Chrome, Firefox, and Safari. - π Security: the page is a static in-process snapshot. It makes no GitHub calls and never contains your token or any secret β see SECURITY.md.
Your queue always in the macOS top bar β a πΉ + open count, click for a dropdown of every bucket with each PR/issue deep-linked to GitHub. The bar tints orange when a review is owed or a PR has gone quiet.
stdlib Ruby can't draw a status item, so this rides on
SwiftBar (or xbar) β a tiny
app that runs a script on an interval and renders its stdout in the menu bar.
--menubar emits exactly that plugin document (one line = the bar title, ---
opens the dropdown, -- nests a row, | href= color= set params). SwiftBar is
an external app, like cloudflared β not a Ruby dependency.
brew install swiftbar # then launch it once and pick a plugin folder
./menubar/install-menubar.sh 5m # render the plugin into that folder, refresh every 5m
./menubar/uninstall-menubar.sh # remove itThe installer fills in this machine's repo/ruby/home and points
KAMANDAR_CONFIG at the repo .env (token + login), so the plugin loads
credentials the same way the launchd service does. Long buckets cap at 12 rows
with an "β¦and N more" link to the web app. Like every surface, the token never
appears in the output.
kamandar --menubar # print the plugin document yourself (one shot)
kamandar --menubar --demo # fabricated data, no token β preview the layoutKeying off reviewDecision == REVIEW_REQUIRED is wrong: after a reviewer
requests changes and the author pushes fixes, reviewDecision stays
CHANGES_REQUESTED until the reviewer re-reviews β so the PR you most want
flagged gets dropped. kamandar uses a timestamp race instead.
flowchart TD
A["handoff = max(last review-requested, last push, PR created)"] --> C{ball on reviewer?}
B["reviewer action = latest APPROVED / CHANGES_REQUESTED<br/>(plain comments ignored)"] --> C
C -->|"handoff > action, or never acted"| D{"days since handoff β₯ STALE_DAYS<br/>and not a draft?"}
C -->|"action newer"| E["not stale (ball on author)"]
D -->|yes| F["β³ STALE"]
D -->|no| G["not stale yet"]
| Scenario | Result |
|---|---|
| Fresh, awaiting review | β³ stale |
| Changes requested, not yet fixed | β not stale (ball on author) |
| Changes requested, then pushed | β³ stale |
| Approved, no new commits | β not stale |
| Approved, then pushed | β³ stale |
| No reviewer at all | π forgot-reviewer (not stale) |
No scheduler code lives in the tool. Wire terminal output into your own cron β e.g. weekday mornings at 8:30, emailed to yourself:
30 8 * * 1-5 GITHUB_TOKEN=... GH_LOGIN=you PROJECT_URL=... \
ruby /path/lib/kamandar.rb | mail -s "Kamandar" you@example.comSwap mail for notify-send (Linux desktop) or terminal-notifier (macOS).
Browser mode is for interactive/ambient use (optionally with --watch), not cron.
Every acceptance scenario is encoded with a fixed "today" (Monday 2026-06-22) and fabricated fixtures β zero network.
ruby test/test_kamandar.rb
# ...
# 254 passed, 0 failedA v2 that abstracts the provider layer to support GitLab and other project managers (Jira, Linear) is sketched in V2.md.
- The saved view filter DSL is not replicated; #3 is approximated by Status (+ optional iteration). Only org + project number are read from the URL.
- "Commented" reviews are intentionally ignored β a comment doesn't flip the ball.
- Any push (incl. a typo fix or rebase/force-push) resets the #4 clock by design
("you resubmitted"). To reset only on an explicit re-request, drop
last_pushfromhandoff_atin the engine. - Browser mode is a static snapshot rendered in-process: no client-side
GitHub calls, no live data except via
--watchre-runs. The token never reaches the page. --serveis a single-user, localhost-only convenience: plain HTTP bound to127.0.0.1, no auth, one request at a time. Don't expose it to a network or proxy it to a public address β see SECURITY.md.- Single user, single token, no multi-tenant concerns.
See CONTRIBUTING.md. Security policy in SECURITY.md.
MIT Β© 2026 cdrrazan
