sigrank 0.15.5 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
+ # SigRank MCP
2
+
1
3
  > **🏆 SigRank is live: [signalaf.com](https://signalaf.com)** — the leaderboard for how
2
4
  > efficiently you use AI, not how much. See your projected rank in 60 seconds at
3
- > [signalaf.com/score](https://signalaf.com/score). *Token counts only. Never your prompts.*
5
+ > [signalaf.com/score](https://signalaf.com/score). _Token counts only. Never your prompts._
4
6
 
5
7
  <div align="center">
6
8
 
@@ -12,6 +14,9 @@ For all builders, burners and 10xers.
12
14
 
13
15
  [![npm version](https://img.shields.io/npm/v/sigrank.svg?style=flat-square&color=gold)](https://www.npmjs.com/package/sigrank)
14
16
  [![CI](https://github.com/SunrisesIllNeverSee/sigrank-mcp/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/SunrisesIllNeverSee/sigrank-mcp/actions/workflows/ci.yml)
17
+ [![CodeQL](https://github.com/SunrisesIllNeverSee/sigrank-mcp/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/SunrisesIllNeverSee/sigrank-mcp/actions/workflows/codeql.yml)
18
+ [![audit](https://github.com/SunrisesIllNeverSee/sigrank-mcp/actions/workflows/dependency-audit.yml/badge.svg?branch=main)](https://github.com/SunrisesIllNeverSee/sigrank-mcp/actions/workflows/dependency-audit.yml)
19
+ [![Dependabot](https://img.shields.io/badge/Dependabot-enabled-025E8C?style=flat-square&logo=dependabot)](https://github.com/SunrisesIllNeverSee/sigrank-mcp/network/dependencies)
15
20
  [![license](https://img.shields.io/badge/license-MIT-green.svg?style=flat-square)](./LICENSE)
16
21
  [![platform](https://img.shields.io/badge/platform-node-grey.svg?style=flat-square)](https://nodejs.org)
17
22
  [![live](https://img.shields.io/badge/board-signalaf.com-gold.svg?style=flat-square)](https://signalaf.com)
@@ -20,10 +25,27 @@ For all builders, burners and 10xers.
20
25
 
21
26
  </div>
22
27
 
23
- | The board | Your operator profile |
24
- |:---:|:---:|
25
- | [![SigRank leaderboard](./.github/assets/board.png)](https://signalaf.com/board/all) | [![SigRank operator profile](./.github/assets/profile.png)](https://signalaf.com) |
26
- | Every operator ranked by **Υ Yield** — the architecture of the cascade, not raw spend | Cascade layer, class, and fingerprint — derived from four token counts |
28
+ ## Table of Contents
29
+
30
+ - [Quickstart](#quickstart--3-steps-to-the-board)
31
+ - [Install from GitHub](#install-from-github)
32
+ - [Install via Smithery](#install-via-smithery)
33
+ - [Commands](#commands)
34
+ - [MCP Server mode](#mcp-server-mode)
35
+ - [Cascade math](#cascade-math)
36
+ - [Token Pillars](#token-pillars--sources)
37
+ - [Platform adapters](#platform-adapters)
38
+ - [Privacy](#privacy)
39
+ - [Env vars](#env-vars)
40
+ - [Dev / test](#dev--test)
41
+ - [File map](#file-map)
42
+ - [Contributing](#contributing)
43
+ - [License](#license)
44
+
45
+ | The board | Your operator profile |
46
+ | :-----------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------: |
47
+ | [![SigRank leaderboard](./.github/assets/board.png)](https://signalaf.com/board/all) | [![SigRank operator profile](./.github/assets/profile.png)](https://signalaf.com) |
48
+ | Every operator ranked by **Υ Yield** — the architecture of the cascade, not raw spend | Cascade layer, class, and fingerprint — derived from four token counts |
27
49
 
28
50
  > **Run [`sigrank enroll`](#sign-in--submit) then [`sigrank submit`](#sign-in--submit) to get ranked and claim your public profile at [signalaf.com](https://signalaf.com).**
29
51
 
@@ -110,7 +132,7 @@ smithery tool call sigrank rank_paste '{"text": "1000000 500000 50000 800000"}'
110
132
  ## Commands
111
133
 
112
134
  ```
113
- ⊙ SigRank CLI v0.14.2
135
+ ⊙ SigRank CLI v0.16.0
114
136
 
115
137
  Default (no args)
116
138
  sigrank unified dashboard: cascade + token pillars + board
@@ -125,11 +147,12 @@ Commands
125
147
  compare --platform codex compare for a specific platform
126
148
  tui full tabbed TUI: Dashboard / Trends / Compare / Board / Watch / Connect
127
149
  tui --platform codex TUI with a different default platform
128
- watch live tune meter — re-reads local logs every 30s
129
- watch --window 7d watch a specific window
150
+ watch live tune meter — ALL active platforms × all windows, every 30s
151
+ watch --platform codex watch only one platform (optional filter)
152
+ watch --window 7d watch only one window (optional filter)
130
153
 
131
154
  Options
132
- --window 7d · 30d · 90d · all (default: 30d for board, 7d for watch)
155
+ --window 7d · 30d · 90d · all (default: 30d for board; all windows for watch)
133
156
  --platform claude · codex · amp · gemini · opencode · goose · …
134
157
  --refresh poll interval in seconds (default: 30)
135
158
  --once print once and exit (board only)
@@ -158,14 +181,14 @@ npx sigrank
158
181
 
159
182
  Six tabs. Keys: `1`-`6` or `←` `→` to switch · `R` refresh · `Q` quit.
160
183
 
161
- | Tab | Key | Content |
162
- |---|---|---|
163
- | **Dashboard** | `1` | Cascade table (all platforms × windows + combined) · Υ sparklines · token composition bars · mini board |
164
- | **Trends** | `2` | Every metric across windows — sub-views: You / Platform / Field |
165
- | **Compare** | `3` | 4-source pillar audit (tokenpull vs ccusage vs token-dash vs tokscale) · delta % · cascade metrics per source · cache read bar chart |
166
- | **Board** | `4` | Full leaderboard with all fields · `[W]` cycles window (7d/30d/90d/all) |
167
- | **Watch** | `5` | In-TUI landing panel · `[Enter]` launches the live watcher (big numbers + pillar bars + Υ trend, auto-refreshes 30s) |
168
- | **Connect** | `6` | Sign in / switch device — paste a connect code from signalaf.com → Settings. Then `[S]` submits. |
184
+ | Tab | Key | Content |
185
+ | ------------- | --- | ------------------------------------------------------------------------------------------------------------------------------------ |
186
+ | **Dashboard** | `1` | Cascade table (all platforms × windows + combined) · Υ sparklines · token composition bars · mini board |
187
+ | **Trends** | `2` | Every metric across windows — sub-views: You / Platform / Field |
188
+ | **Compare** | `3` | 4-source pillar audit (tokenpull vs ccusage vs token-dash vs tokscale) · delta % · cascade metrics per source · cache read bar chart |
189
+ | **Board** | `4` | Full leaderboard with all fields · `[W]` cycles window (7d/30d/90d/all) |
190
+ | **Watch** | `5` | In-TUI landing panel · `[Enter]` launches the live watcher (big numbers + pillar bars + Υ trend, auto-refreshes 30s) |
191
+ | **Connect** | `6` | Sign in / switch device — paste a connect code from signalaf.com → Settings. Then `[S]` submits. |
169
192
 
170
193
  ### Sign in + submit
171
194
 
@@ -184,6 +207,7 @@ Or do it inside the TUI on the **Connect** tab (`6`), then press `[S]` to submit
184
207
  When stdout is not a TTY (i.e. piped to an AI client), `sigrank` starts an MCP stdio server automatically. AI clients (Claude Code, Cursor, Windsurf, etc.) use this path.
185
208
 
186
209
  Add to `.mcp.json` or equivalent:
210
+
187
211
  ```json
188
212
  {
189
213
  "mcpServers": {
@@ -194,7 +218,9 @@ Add to `.mcp.json` or equivalent:
194
218
  }
195
219
  }
196
220
  ```
221
+
197
222
  Or if installed globally:
223
+
198
224
  ```json
199
225
  {
200
226
  "mcpServers": {
@@ -207,19 +233,23 @@ Or if installed globally:
207
233
 
208
234
  ### MCP Tools
209
235
 
210
- | Tool | Args | What |
211
- |---|---|---|
212
- | `rank_paste(text)` | `{input, output, cacheCreate, cacheRead}` JSON or 4 whitespace-delimited numbers | Scores token pillars → Υ Yield / SNR / Leverage / Velocity / 10xDEV / Class + prose narration card |
213
- | `get_leaderboard()` | `{window?}` | Live board from signalaf.com — sorted by Υ Yield |
214
- | `get_operator(codename)` | `{codename}` | One operator's live profile |
215
- | `submit_paste(text, codename)` | `{text, codename?}` | Rank locally then POST to board. Omit codename for preview-only |
216
- | `tokenpull(platform?)` | `{platform?}` | On-device local reader: scans local logs → 4-window cascade. Zero paste, token-only |
217
- | `tokenpull_submit(codename, window?)` | `{codename?, window?}` | `tokenpull` → publish to board. Omit codename for preview |
218
- | `tokenpull_compare(platform?)` | `{platform?}` | All four sources side-by-side: tokenpull + ccusage + token-dash + tokscale. Returns pillars, cascade metrics, and delta % vs tokenpull per window |
219
- | `rank_windows` | `{platform?, window?}` | Multi-window cascade from local logs |
220
- | `watch_tokenpull` | `{platform?, interval_s?}` | One cascade snapshot per call (interval_s advisory) |
221
- | `submit_verified` | `{window?, platform?, dry_run?}` | THE ranked path: builds + ed25519-signs Schema 1.0 snapshots and POSTs them. `platform:'multi'` sums all active platforms. `dry_run:true` returns the exact payload unsent |
222
- | `enroll` | `{code, device_label?}` | Bind this device with a connect code from signalaf.com → Settings |
236
+ | Tool | Args | What |
237
+ | ------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
238
+ | `rank_paste(text)` | `{input, output, cacheCreate, cacheRead}` JSON or 4 whitespace-delimited numbers | Scores token pillars → Υ Yield / SNR / Leverage / Velocity / 10xDEV / Class + prose narration card |
239
+ | `get_leaderboard()` | `{window?}` | Live board from signalaf.com — sorted by Υ Yield |
240
+ | `get_operator(codename)` | `{codename}` | One operator's live profile |
241
+ | `submit_paste(text, codename)` | `{text, codename?}` | Rank locally then POST to board. Omit codename for preview-only |
242
+ | `tokenpull(platform?)` | `{platform?}` | On-device local reader: scans local logs → 4-window cascade. Zero paste, token-only |
243
+ | `tokenpull_submit(codename, window?)` | `{codename?, window?}` | `tokenpull` → publish to board. Omit codename for preview |
244
+ | `tokenpull_compare(platform?)` | `{platform?}` | All four sources side-by-side: tokenpull + ccusage + token-dash + tokscale. Returns pillars, cascade metrics, and delta % vs tokenpull per window |
245
+ | `rank_windows` | `{platform?, window?}` | Multi-window cascade from local logs |
246
+ | `watch_tokenpull` | `{platform?, interval_s?}` | One cascade snapshot per call (interval_s advisory) |
247
+ | `submit_verified` | `{window?, platform?, dry_run?}` | THE ranked path: builds + ed25519-signs Schema 1.0 snapshots and POSTs them. `platform:'multi'` sums all active platforms. `dry_run:true` returns the exact payload unsent |
248
+ | `enroll` | `{code, device_label?}` | Bind this device with a connect code from signalaf.com → Settings |
249
+ | `diagnose_cascade` | `{text?}` | Diagnoses where your token cascade is leaking efficiency — ranked findings with severity + estimated Υ impact |
250
+ | `simulate_change` | `{text?, changes}` | Prescriptive "what if" — test proposed pillar changes and see the exact Υ delta + class change before committing |
251
+ | `suggest_improvements` | `{text?}` | Generates ranked, simulated improvement suggestions — tests strategies and returns them sorted by Υ yield impact |
252
+ | `self_improve` | `{text?}` | One-click optimize: diagnoses, suggests, and simulates the best change in a single call |
223
253
 
224
254
  ---
225
255
 
@@ -242,19 +272,21 @@ Canon check: `MO§ES (1251211, 11296121, 128196310, 2555179769) → Υ 18436.98`
242
272
 
243
273
  The dashboard pulls from multiple sources and shows them side-by-side for verification:
244
274
 
245
- | Source | What | Platform |
246
- |---|---|---|
247
- | `tokenpull` | On-device JSONL scanner (canon source) | claude, codex, amp, … |
248
- | `ccusage` | `ccusage <platform> daily --json` CLI (bundled) | claude, codex |
249
- | `token-dashboard` | `~/.claude/token-dashboard.db` SQLite (bundled) | claude only |
250
- | `tokscale` | `tokscale models --json` CLI (bundled, falls back to `~/tokscale_report.json`) | claude, codex |
275
+ | Source | What | Platform |
276
+ | ----------------- | ------------------------------------------------------------------------------ | --------------------- |
277
+ | `tokenpull` | On-device JSONL scanner (canon source) | claude, codex, amp, … |
278
+ | `ccusage` | `ccusage <platform> daily --json` CLI (bundled) | claude, codex |
279
+ | `token-dashboard` | `~/.claude/token-dashboard.db` SQLite (bundled) | claude only |
280
+ | `tokscale` | `tokscale models --json` CLI (bundled, falls back to `~/tokscale_report.json`) | claude, codex |
251
281
 
252
282
  **Codex input is estimated** — Codex logs don't expose true input tokens directly. The formula:
283
+
253
284
  ```
254
285
  input = output × ioRatio (ioRatio derived from Claude ratio, else 2.0)
255
286
  cacheCreate = uncached − input (uncached = input_tokens − cached_input_tokens)
256
287
  cacheRead = exact (from logs)
257
288
  ```
289
+
258
290
  Verifier numbers (ccusage/tokscale for codex) show **raw uncached input** (`input_tokens − cached`) — a different field than the estimated input above. The discrepancy is expected and explained inline in the dashboard.
259
291
 
260
292
  ---
@@ -263,25 +295,25 @@ Verifier numbers (ccusage/tokscale for codex) show **raw uncached input** (`inpu
263
295
 
264
296
  All adapters are token-only (no message content, no cost fields, no credentials).
265
297
 
266
- | Platform | Path | Notes |
267
- |---|---|---|
268
- | Claude Code | ✅ `~/.claude/projects` | Native; dedup by `(session_id, message_id)`; subagents included |
269
- | Codex | ✅ `~/.codex/sessions` | Estimated input via `io_ratio`; verified vs ccusage |
270
- | Amp | ✅ `~/.local/share/amp/threads` | Full 4-pillar; per-message |
271
- | Kimi | ✅ `~/.kimi/sessions` | Full 4-pillar; `StatusUpdate` lines only |
272
- | pi-agent | ✅ `~/.pi/agent/sessions` | Full 4-pillar; per-message JSONL |
273
- | OpenClaw | ✅ `~/.openclaw` | Full 4-pillar; per-message JSONL |
274
- | Droid | ✅ `~/.factory/sessions/*.settings.json` | Full 4-pillar; thinking→output |
275
- | Codebuff | ✅ `~/.config/manicode` | Full 4-pillar; `chat-messages.json` |
276
- | Hermes | ✅ `~/.hermes/state.db` | Full 4-pillar; SQLite; reasoning→output |
277
- | Kilo | ✅ `~/.local/share/kilo/kilo.db` | Full 4-pillar; SQLite |
278
- | Qwen | ✅ `~/.qwen/projects` | `cacheCreate=0` estimated; thought→output |
279
- | Goose | ✅ `~/.local/share/goose/sessions/sessions.db` | `cacheCreate=cacheRead=0` estimated; SQLite |
280
- | Gemini CLI | ✅ `~/.gemini/tmp` | `cacheCreate=0` estimated; cache extracted from input field |
281
- | GitHub Copilot CLI | ✅ `~/.copilot/otel` | OTel JSONL; requires `COPILOT_OTEL_ENABLED=true` |
282
- | OpenCode | ⚠️ `~/.local/share/opencode` | Raw token counts not persisted in log format |
283
- | Cursor | 🔜 | Chat log path TBD |
284
- | Windsurf | 🔜 | Session logs at `~/.codeium/windsurf/` |
298
+ | Platform | Path | Notes |
299
+ | ------------------ | ---------------------------------------------- | --------------------------------------------------------------- |
300
+ | Claude Code | ✅ `~/.claude/projects` | Native; dedup by `(session_id, message_id)`; subagents included |
301
+ | Codex | ✅ `~/.codex/sessions` | Estimated input via `io_ratio`; verified vs ccusage |
302
+ | Amp | ✅ `~/.local/share/amp/threads` | Full 4-pillar; per-message |
303
+ | Kimi | ✅ `~/.kimi/sessions` | Full 4-pillar; `StatusUpdate` lines only |
304
+ | pi-agent | ✅ `~/.pi/agent/sessions` | Full 4-pillar; per-message JSONL |
305
+ | OpenClaw | ✅ `~/.openclaw` | Full 4-pillar; per-message JSONL |
306
+ | Droid | ✅ `~/.factory/sessions/*.settings.json` | Full 4-pillar; thinking→output |
307
+ | Codebuff | ✅ `~/.config/manicode` | Full 4-pillar; `chat-messages.json` |
308
+ | Hermes | ✅ `~/.hermes/state.db` | Full 4-pillar; SQLite; reasoning→output |
309
+ | Kilo | ✅ `~/.local/share/kilo/kilo.db` | Full 4-pillar; SQLite |
310
+ | Qwen | ✅ `~/.qwen/projects` | `cacheCreate=0` estimated; thought→output |
311
+ | Goose | ✅ `~/.local/share/goose/sessions/sessions.db` | `cacheCreate=cacheRead=0` estimated; SQLite |
312
+ | Gemini CLI | ✅ `~/.gemini/tmp` | `cacheCreate=0` estimated; cache extracted from input field |
313
+ | GitHub Copilot CLI | ✅ `~/.copilot/otel` | OTel JSONL; requires `COPILOT_OTEL_ENABLED=true` |
314
+ | OpenCode | ⚠️ `~/.local/share/opencode` | Raw token counts not persisted in log format |
315
+ | Cursor | 🔜 | Chat log path TBD |
316
+ | Windsurf | 🔜 | Session logs at `~/.codeium/windsurf/` |
285
317
 
286
318
  `estimated=true` means one or more pillars are derived, not native. The server re-scores all submitted pillars authoritatively; local preview Υ is indicative only.
287
319
 
@@ -299,21 +331,23 @@ All adapters are token-only (no message content, no cost fields, no credentials)
299
331
 
300
332
  ## Env vars
301
333
 
302
- | Var | Default | Description |
303
- |---|---|---|
304
- | `SIGRANK_API_BASE` | `https://signalaf.com` | Override the board host |
305
- | `SIGRANK_FETCH_TIMEOUT` | `10000` | Board API fetch timeout (ms) |
334
+ | Var | Default | Description |
335
+ | ----------------------- | ---------------------- | ---------------------------- |
336
+ | `SIGRANK_API_BASE` | `https://signalaf.com` | Override the board host |
337
+ | `SIGRANK_FETCH_TIMEOUT` | `10000` | Board API fetch timeout (ms) |
306
338
 
307
339
  ---
308
340
 
309
341
  ## Dev / test
310
342
 
311
343
  ```bash
312
- node test.mjs # 29 unit tests (no network, no fs writes)
344
+ node test.mjs # 13 test groups, 200 assertions (no network, no fs writes)
345
+ node sign.test.mjs # ed25519 signing + canon parity
313
346
  node index.mjs # stdio MCP server directly (pipe to MCP client)
314
347
  ```
315
348
 
316
- Tests verify (29 assertions):
349
+ Tests verify (13 groups, 200 assertions):
350
+
317
351
  - `rank_paste` canon: MO§ES `(1251211, 11296121, 128196310, 2555179769)` → Υ 18436.98 · TRANSMITTER
318
352
  - `submit_paste` preview (no codename) + POST shape (injected fetch, no live writes)
319
353
  - `tokenpull` dedup, window slicing, 4-window pillars (mock adapter)
@@ -321,25 +355,46 @@ Tests verify (29 assertions):
321
355
  - `tokenpullCodex` io_ratio conversion per-window
322
356
  - Adapter registry (15 platforms) + per-adapter shape contracts
323
357
  - `rank_windows` 4-window paste scoring, partial input, no-network
324
- - `watch_tokenpull` cascade snapshot, interval_s, TODO(AUTH.WIRE) stub
358
+ - `watch_tokenpull` cascade snapshot, interval_s, submit path
359
+ - `enroll` posts identity (public key only), maps 201 enrolled + 410 code_invalid
360
+ - `submit_verified` signs Schema 1.0, server-verifiable
361
+ - `simulate_change` relative + absolute deltas, quadratic penalty, JSON input
325
362
  - Hardening: div-by-zero guards, parsePillars warnings, fetch timeout, EXCLUDE_TOOLING regex, narrate safety
363
+ - `sign.test.mjs` ed25519 round-trip + canonical 926-byte payload parity
326
364
 
327
365
  ---
328
366
 
329
367
  ## File map
330
368
 
331
- | File | Responsibility |
332
- |---|---|
333
- | `index.mjs` | Entry point — TTY detection, routes to CLI or MCP server |
334
- | `cli.mjs` | CLI commands: board, compare, watch, enroll, submit, help |
335
- | `tui.mjs` | Full tabbed TUI: Dashboard / Trends / Compare / Board / Watch / Connect |
336
- | `cascade.mjs` | Pure cascade math (Υ, SNR, leverage, velocity, 10xDEV, class) |
337
- | `tokenpull.mjs` | On-device log scanner — Claude, Codex, multi-platform |
338
- | `adapters.mjs` | Platform adapter registry (15+ platforms) |
339
- | `tools.mjs` | MCP tool table + dispatcher |
340
- | `connect.mjs` | Connect-code enrollment + device identity |
341
- | `keystore.mjs` | Local key management (paste-keys, not API keys) |
342
- | `submit.mjs` | Verified submit flow (signs + POSTs to board) |
343
- | `sign.mjs` | Schema 1.0 signing (X-Agent-Signature) |
344
- | `narrate.mjs` | Deterministic prose narration card |
345
- | `test.mjs` | Unit tests (no external deps) |
369
+ | File | Responsibility |
370
+ | --------------- | ----------------------------------------------------------------------- |
371
+ | `index.mjs` | Entry point — TTY detection, routes to CLI or MCP server |
372
+ | `cli.mjs` | CLI commands: board, compare, watch, enroll, submit, help |
373
+ | `tui.mjs` | Full tabbed TUI: Dashboard / Trends / Compare / Board / Watch / Connect |
374
+ | `cascade.mjs` | Pure cascade math (Υ, SNR, leverage, velocity, 10xDEV, class) |
375
+ | `tokenpull.mjs` | On-device log scanner — Claude, Codex, multi-platform |
376
+ | `adapters.mjs` | Platform adapter registry (15+ platforms) |
377
+ | `tools.mjs` | MCP tool table + dispatcher |
378
+ | `connect.mjs` | Connect-code enrollment + device identity |
379
+ | `keystore.mjs` | Local key management (paste-keys, not API keys) |
380
+ | `submit.mjs` | Verified submit flow (signs + POSTs to board) |
381
+ | `sign.mjs` | Schema 1.0 signing (X-Agent-Signature) |
382
+ | `narrate.mjs` | Deterministic prose narration card |
383
+ | `preflight.mjs` | Plausibility checks (Benford, bounds, anomaly detection) |
384
+ | `test.mjs` | Unit tests (no external deps) |
385
+ | `sign.test.mjs` | ed25519 signing + canon parity test |
386
+
387
+ ---
388
+
389
+ ## Contributing
390
+
391
+ Contributions welcome. SigRank MCP is built in the open.
392
+
393
+ - [Contributing guide](./.github/CONTRIBUTING.md)
394
+ - [Security policy](https://github.com/SunrisesIllNeverSee/sigrank-mcp/security/policy)
395
+ - Report bugs via [GitHub Issues](https://github.com/SunrisesIllNeverSee/sigrank-mcp/issues)
396
+ - PRs: fork → branch → `node test.mjs` passes → open PR against `main`
397
+
398
+ ## License
399
+
400
+ MIT — see [`LICENSE`](./LICENSE).