austere-axi 0.1.1 → 0.3.0

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,95 +1,102 @@
1
1
  # austere-axi
2
2
 
3
- Review artifacts for coding agents, written in [OpenUI Lang](https://github.com/thesysdev/openui) instead of hand-written HTML.
3
+ [![npm version](https://img.shields.io/npm/v/austere-axi)](https://www.npmjs.com/package/austere-axi)
4
+ [![npm downloads](https://img.shields.io/npm/dm/austere-axi)](https://www.npmjs.com/package/austere-axi)
5
+ [![jsDelivr hits](https://img.shields.io/jsdelivr/npm/hm/austere-axi)](https://www.jsdelivr.com/package/npm/austere-axi)
6
+ [![skills.sh](https://skills.sh/b/mandalorianbatman/austere-axi)](https://skills.sh/mandalorianbatman/austere-axi)
7
+ [![CI](https://github.com/MandalorianBatman/austere-axi/actions/workflows/ci.yml/badge.svg)](https://github.com/MandalorianBatman/austere-axi/actions/workflows/ci.yml)
8
+ [![node](https://img.shields.io/node/v/austere-axi)](https://www.npmjs.com/package/austere-axi)
9
+ [![license](https://img.shields.io/npm/l/austere-axi)](LICENSE)
4
10
 
5
- The agent writes a short program: one named statement per line, built from a fixed set of components. The reviewer opens it in the browser, points at anything, and the agent gets the feedback back addressed by statement name. To revise, the agent edits only the statements the feedback names, and the page updates in place.
6
-
7
- It is the same loop as [lavish-axi](https://github.com/kunchenguid/lavish-axi). The difference is that the agent never writes CSS or layout.
11
+ Review pages for coding agents. The agent writes a short [OpenUI Lang](https://github.com/thesysdev/openui) program, not HTML. You open it in the browser, click on any part, and write a note. The agent gets each note with the name of the statement it points at. It edits only those statements, and the page updates in place.
8
12
 
9
13
  ```
10
14
  root = Stack([intro, flow, options, decide], "column", "l")
11
15
  intro = Card([CardHeader("Session storage migration", "Redis → Postgres"), introText])
12
16
  introText = MarkDownRenderer("Sessions are lost on failover. **Goal:** durable sessions, no forced logout.")
13
17
  flow = Card([CardHeader("Request path"), flowDiagram])
14
- flowDiagram = Mermaid("flowchart LR
15
- client --> edge --> api --> pg[(Postgres)]")
18
+ flowDiagram = Architecture([client, api, pg], [toApi, toPg])
19
+ client = Node("Client", "external")
20
+ api = Node("API", "backend", "Node :8080")
21
+ pg = Node("Postgres", "database", "sessions table", "new")
22
+ toApi = Edge(client, api, "HTTPS", "emphasis")
23
+ toPg = Edge(api, pg, "SQL")
16
24
  options = Card([CardHeader("Rollout options"), optionTable])
17
25
  optionTable = Table([Col("Option", ["Big bang", "Dual-write"]), Col("Risk", [Tag("high", null, "sm", "danger"), Tag("low", null, "sm", "success")])])
18
26
  decide = Form("rollout", Buttons([Button("Send decision", @ToAssistant("Decision: rollout"), "primary")]), [strategyField])
19
27
  strategyField = FormControl("Rollout", RadioGroup("strategy", [RadioItem("Big bang", "", "bigbang"), RadioItem("Dual-write", "", "dualwrite")]))
20
28
  ```
21
29
 
22
- [`examples/session-migration.html`](examples/session-migration.html) is a full page: a KPI row, a diagram, a table, a chart, a diff, and a decision form. The whole file is about 900 tokens.
30
+ [`examples/session-migration.html`](examples/session-migration.html) is a full page (KPIs, diagram, table, chart, diff, decision form) in about 1,000 tokens.
23
31
 
24
32
  ## Why
25
33
 
26
- Lavish makes the agent author an entire HTML page every time, and re-author it after every round of feedback. That is slow, because output tokens are the slow part, and it is expensive.
27
-
28
- austere-axi changes three things:
34
+ The loop is the same as [lavish-axi](https://github.com/kunchenguid/lavish-axi). The difference: the agent never writes HTML, CSS or layout.
29
35
 
30
36
  | | lavish-axi | austere-axi |
31
37
  |---|---|---|
32
- | What the agent writes | Full HTML + CSS + inline SVG | OpenUI Lang statements |
33
- | Revising after feedback | Regenerate the file | Edit the named statements; the page patches in place and highlights what changed |
34
- | Feedback target | CSS selector + text | Statement name, its line in the file, plus cell / diff line / diagram node / selected text |
35
- | Validation | In-browser layout audit | `austere-axi check`: the renderer's own parser, in Node, no browser |
36
- | Look | Whatever the agent designs | One consistent dark design across agents |
38
+ | Agent writes | Full HTML + CSS + SVG | OpenUI Lang statements |
39
+ | After feedback | Writes the file again | Edits the named statements; the page patches in place |
40
+ | Feedback points at | CSS selector + text | Statement name and line, plus table cell, diff line, diagram node or selected text |
41
+ | Validation | Layout audit in the browser | `austere-axi check`, in Node, no browser |
42
+ | Look | Different for each agent | One dark design |
37
43
 
38
- ## How it works
39
-
40
- ```
41
- agent writes plan.html ──► austere-axi plan.html ──► local server + browser review
42
- │ reviewer annotates, picks options,
43
- │ types, presses Send
44
- agent edits statements ◄── austere-axi poll plan.html ◄─┘ (long-poll returns the batch)
45
- │
46
- └─► page updates in place and flashes the changed statements
47
- ```
48
-
49
- - **An artifact is a plain `.html` file.** It contains one `<script>` tag for the runtime and one `<script type="text/openui">` block holding the program. Opened directly, it renders from the CDN. Under review, the server points that tag at a local copy.
50
- - **The runtime** bundles the OpenUI React renderer with a curated component library: 58 of the stock components, plus `Mermaid`, `Diff` and `FileTree`. Every rendered statement is tagged with its name.
51
- - **Annotations** resolve to the innermost named statement. They also carry precise detail where it exists: table row and column, `file:+line` for diff lines, the label of a diagram node, or the selected text. The popover shows the parent chain, so the reviewer can retarget a note to an enclosing section.
52
- - **Buttons and forms** are feedback channels. `@ToAssistant("…")` queues a message along with the form's values. A button with no action sends its label.
53
- - **Remote review.** When Tailscale is running, the server also listens on this machine's tailnet IP, and `open` prints a phone link. The review UI has a mobile layout with a conversation sheet.
44
+ Fewer output tokens means faster pages and lower cost.
54
45
 
55
46
  ## Install
56
47
 
57
- Requires Node.js 20 or later.
48
+ You need Node.js 20 or later.
58
49
 
59
- **Run it without installing** (what agents use):
50
+ **Let your agent do it.** Install the setup skill, then ask your agent to "set up austere-axi":
60
51
 
61
52
  ```sh
62
- npx -y austere-axi --help
53
+ npx skills add MandalorianBatman/austere-axi --skill austere-setup
63
54
  ```
64
55
 
65
- `npx` downloads the package into npm's cache and runs it. `-y` skips the install prompt, so a non-interactive agent never stalls on it. Prefix every command below the same way: `npx -y austere-axi spec`, `npx -y austere-axi plan.html`, and so on.
56
+ It checks Node, installs the `austere` skill, offers the optional items below, and opens a test page.
66
57
 
67
- **Or install the command globally:**
58
+ **Or do it yourself.** Agents can run the CLI with no install:
68
59
 
69
60
  ```sh
70
- npm install -g austere-axi
71
- austere-axi --help
61
+ npx -y austere-axi --help
72
62
  ```
73
63
 
74
- A global install stays on the version you installed until `npm update -g austere-axi`. Mixing the two is fine: a newer CLI replaces an older background server automatically.
75
-
76
- **Agent skill (optional):**
64
+ `-y` skips the npm prompt, so an agent does not stop on it. Optional:
77
65
 
78
66
  ```sh
79
- npx skills add MandalorianBatman/austere-axi --skill austere
67
+ npm install -g austere-axi # `austere-axi` command, no npx
68
+ npx skills add MandalorianBatman/austere-axi --skill austere # tells agents when to make a page; adds /austere
80
69
  ```
81
70
 
82
- The skill tells an agent when a page beats prose and adds an `/austere <request>` command. It is a stub that sends the agent to `npx -y austere-axi --help` and `spec`, so an installed copy cannot go stale. Agents that can run shell commands do not need it.
71
+ The `austere` skill is a stub that points at `--help` and `spec`, so it does not go out of date. A global install stays on its version until `npm update -g austere-axi`.
83
72
 
84
73
  ## Quick start
85
74
 
86
75
  ```sh
87
- npx -y austere-axi spec # component reference for the agent (read once per session)
76
+ npx -y austere-axi spec # component reference (read once per session)
88
77
  npx -y austere-axi plan.html # validate and open the page for review
89
- npx -y austere-axi poll plan.html # wait for the reviewer to press Send
78
+ npx -y austere-axi poll plan.html # wait until the reviewer presses Send
90
79
  ```
91
80
 
92
- `--help` prints the file skeleton and the full review loop.
81
+ `--help` prints the file skeleton and the full loop.
82
+
83
+ ## How it works
84
+
85
+ ```
86
+ agent writes plan.html ──► austere-axi plan.html ──► local server + browser review
87
+ │ reviewer annotates, picks options,
88
+ │ types, presses Send
89
+ agent edits statements ◄── austere-axi poll plan.html ◄─┘ (long-poll returns the batch)
90
+ │
91
+ └─► page updates in place and flashes the changed statements
92
+ ```
93
+
94
+ - **A page is a plain `.html` file**: one runtime `<script>` tag and one `<script type="text/openui">` block. Opened directly, it loads the runtime from the CDN. Under review, the server serves a local copy.
95
+ - **Components**: 58 stock OpenUI components, plus `Architecture` (`Node`, `Edge`, `Group`), `Diff` and `FileTree`. `Mermaid` is a bundled [pack](#components-and-packs).
96
+ - **Architecture diagrams** lay out automatically ([dagre](https://github.com/dagrejs/dagre)). The agent never writes coordinates. Each node and edge is a click target. `lanes` puts each group in a column; `tone` colors a box or line to show risk. Use Mermaid for sequences, state machines, ER and gantt.
97
+ - **Annotations** go to the smallest named statement under the click, with detail where it exists: table row and column, `file:+line` in a diff, a diagram node, or selected text. The reviewer can move a note to a parent section.
98
+ - **Buttons and forms** send feedback. `@ToAssistant("…")` sends a message with the form values. A button with no action sends its label.
99
+ - **Phone review**: when Tailscale runs, the server also listens on the tailnet IP and prints a phone link. The review UI has a mobile layout.
93
100
 
94
101
  ## CLI
95
102
 
@@ -99,99 +106,139 @@ austere-axi <file> validate + open a review [--no-open] [--reope
99
106
  austere-axi poll <file> long-poll for feedback [--agent-reply "..."] [--takeover]
100
107
  austere-axi reply <file> post a reply without polling
101
108
  austere-axi end <file> end the review as the agent
102
- austere-axi check <file> validate the program (no browser)
109
+ austere-axi feedback <file> feedback no agent has answered yet [--all] [--peek]
110
+ austere-axi check <file> validate the program (no browser) [--fix]
103
111
  austere-axi patch <file> merge statements from stdin into the program
104
- austere-axi spec [group...] component reference (~5.9k tokens; groups narrow it)
112
+ austere-axi spec [group|pack...] component reference (~5.8k tokens; groups narrow it)
113
+ austere-axi components ... list | search | show | add | remove | new | build
105
114
  austere-axi export <file> one self-contained HTML file, works offline
115
+ austere-axi hub on|off|status|list|open persistent server with an index of every page
106
116
  austere-axi stop stop the background server
107
117
  ```
108
118
 
109
- Output is compact YAML, written for agents to read. `poll` returns each item with the statement it points at and that statement's line in the file, so the agent can edit it without re-reading the file.
119
+ Output is compact YAML for agents. Each `poll` item has its statement and that statement's line, so the agent does not need to read the file again.
120
+
121
+ Session rules:
122
+
123
+ - One poll listens on a session at a time.
124
+ - Feedback sent while no agent listens waits for the next poll.
125
+ - A review the user ended does not open again unless you pass `--reopen`.
126
+ - If all review tabs stay closed, `poll` returns `browser_disconnected`: 10 s after a tab closes, or 5 min after a dropped connection (for example, a locked phone).
127
+ - `feedback <file>` returns notes no agent has answered, also from earlier days. Reading them marks them received; `reply` marks them answered.
128
+
129
+ ## Components and packs
130
+
131
+ `spec` covers the common components (~5.8k tokens). Rare ones (area and scatter charts, sliders, switches, editable tables, …) are in the library but not in `spec`. Find them with `components`:
132
+
133
+ ```sh
134
+ austere-axi components # every component, by group
135
+ austere-axi components search scatter # search by name, group, description
136
+ austere-axi components show AreaChart # signature, notes, example
137
+ ```
138
+
139
+ **Packs** add components. A pack is an npm package with `dist/pack.js` (the components, a few KB) and `dist/catalog.json` (signatures, examples, schema).
140
+
141
+ ```sh
142
+ austere-axi components add austere-pack-gantt --trust # npm name, folder, or .tgz
143
+ austere-axi check --fix plan.html # adds the pack tag to the page
144
+ ```
145
+
146
+ A page names its packs in the head, so it renders wherever you open it:
147
+
148
+ ```html
149
+ <meta name="austere-packs" content="austere-pack-gantt@1.2.0 sha384-…">
150
+ ```
151
+
152
+ - Under review, the server serves installed packs only while they match the hash in `~/.austere-axi/packs.json`. Opened directly, the page loads them from jsDelivr with Subresource Integrity. `export` inlines them.
153
+ - A pack component with no pack tag is a `missing-pack` error. `check --fix` repairs it.
154
+ - Mermaid ships as a bundled pack. It loads only when the page calls `Mermaid(...)` and needs no tag.
155
+
156
+ **Trust.** Pack code runs inside your review pages, where notes and button presses also come from. Packs under `@austere-axi/` on npm install directly. Other npm packs and any `.tgz` need `--trust`; read them first. A local folder counts as your own. A page with third-party packs shows a warning, and `poll` tells the agent.
157
+
158
+ **Write a pack:**
159
+
160
+ ```sh
161
+ austere-axi components new austere-pack-timeline
162
+ cd austere-pack-timeline && npm install && npm run build
163
+ austere-axi components add .
164
+ ```
165
+
166
+ Each component uses `austereComponent` from `austere-axi/pack`: a PascalCase name, zod props in positional order (append-only after you publish), a one-line description and an example. The build loads the pack in Node, so build only code you trust. Publish with `npm publish`.
110
167
 
111
- Session rules follow lavish:
168
+ ## Hub
169
+
170
+ The server starts when needed and stops after 30 idle minutes. `hub on` keeps it running and indexes every page you open or check:
171
+
172
+ ```sh
173
+ austere-axi hub on # http://127.0.0.1:4388/: pages by project and day, with search
174
+ austere-axi hub list # the same, for an agent: [words] [--project dir]
175
+ austere-axi hub off # back to on-demand; the index stays
176
+ ```
112
177
 
113
- - Only one poll listens on a session at a time.
114
- - Feedback sent while no agent is listening is queued until the next poll.
115
- - A review the user ended is not reopened unless `--reopen` is passed.
116
- - If every review tab stays closed, `poll` returns `browser_disconnected`. The wait is 10 s after a tab is closed, or 5 min after a dropped connection, such as a phone screen locking.
178
+ The home page shows which reviews are live and how much feedback the agent has answered. The index is SQLite (`~/.austere-axi/hub.db`), copied from the session files; the review loop does not need it. The hub needs Node 22.13 or later (`node:sqlite`).
117
179
 
118
- ### Environment
180
+ ## Environment
119
181
 
120
182
  | Variable | Default | |
121
183
  |---|---|---|
122
184
  | `AUSTERE_AXI_PORT` | `4388` | server port |
123
185
  | `AUSTERE_AXI_STATE_DIR` | `~/.austere-axi` | sessions, server log |
124
- | `AUSTERE_AXI_NO_OPEN` | | `1` = never launch a browser |
125
- | `AUSTERE_AXI_HOST` | | extra concrete bind address (never `0.0.0.0`) |
186
+ | `AUSTERE_AXI_NO_OPEN` | | `1` = never open a browser |
187
+ | `AUSTERE_AXI_HOST` | | extra bind address (never `0.0.0.0`) |
126
188
  | `AUSTERE_AXI_NO_TAILSCALE` | | `1` = loopback only |
127
- | `AUSTERE_AXI_LINK_HOST` | | host to print in the remote link |
189
+ | `AUSTERE_AXI_LINK_HOST` | | host in the printed remote link |
128
190
  | `AUSTERE_AXI_ALLOWED_HOSTS` | | extra `Host` headers to accept (reverse proxy) |
129
- | `AUSTERE_AXI_DISCONNECT_GRACE_MS` | `300000` | how long a dropped connection may stay away before `poll` returns; `0` disables |
130
- | `AUSTERE_AXI_IDLE_TIMEOUT_MS` | `1800000` | idle server shutdown; `0` disables |
191
+ | `AUSTERE_AXI_DISCONNECT_GRACE_MS` | `300000` | how long a dropped connection can stay away before `poll` returns; `0` disables |
192
+ | `AUSTERE_AXI_IDLE_TIMEOUT_MS` | `1800000` | idle shutdown; `0` disables (the hub never idles out) |
193
+ | `AUSTERE_AXI_NPM_REGISTRY` | `npm_config_registry`, else npmjs | where `components add` gets packs |
131
194
 
132
195
  ## Security
133
196
 
134
- The server binds to loopback, plus the Tailscale IP when Tailscale is up. It never binds to every interface.
197
+ The server binds to loopback, plus the Tailscale IP when Tailscale is up. It never binds to all interfaces.
135
198
 
136
- - **CLI control routes** (`/api/*`) answer only loopback callers.
137
- - **Host headers** are checked against an allowlist, as a DNS-rebinding defense.
138
- - **Mutating browser routes** reject foreign `Origin` headers.
139
- - **The artifact** runs in a sandboxed iframe with no same-origin access.
140
- - **File access** covers only the artifact's own directory, with no dotfiles.
199
+ - `/api/*` answers only loopback callers and refuses browser requests.
200
+ - `Host` headers must be on an allowlist (DNS-rebinding defense).
201
+ - Browser routes that change state refuse a foreign `Origin`.
202
+ - The page runs in a sandboxed iframe with no same-origin access. Packs run there too.
203
+ - The server serves files only from the page's own directory, with no dotfiles.
204
+ - Third-party packs need `--trust`. Installed packs must match their lockfile hash; CDN loads use Subresource Integrity.
141
205
 
142
- The Node side of OpenUI ships PostHog telemetry. The CLI sets `OPENUI_TELEMETRY_DISABLED=1` so nothing is sent; the browser runtime never sends telemetry.
206
+ OpenUI's Node side has PostHog telemetry. The CLI sets `OPENUI_TELEMETRY_DISABLED=1`. The browser runtime sends no telemetry.
143
207
 
144
208
  ## Not in v1
145
209
 
146
- These are left out of v1 on purpose:
147
-
148
- - **Image attachments** in feedback.
149
- - **Live data:** `Query` and `Mutation` are disabled, so artifacts are static.
150
- - **Themes:** there is one dark theme. Per-project accents via OpenUI's `ThemeProvider` are an easy follow-up.
151
- - **Hosted sharing:** use `export` to produce a single offline file instead.
210
+ - Image attachments in feedback.
211
+ - Live data: `Query` and `Mutation` are off, so pages are static.
212
+ - Themes: one dark theme only.
213
+ - Hosted sharing: use `export` to make one offline file.
152
214
 
153
215
  ## Development
154
216
 
155
217
  ```sh
156
218
  npm install
157
- npm run build # dist/: austere.js (runtime), mermaid.min.js, spec.json, prompt.md, chrome/, cli.mjs
219
+ npm run build # dist/: runtime, packs, pack kit, Mermaid, spec, catalog, review UI, cli.mjs
158
220
  npm test
159
221
  node dist/cli.mjs examples/session-migration.html
160
222
  ```
161
223
 
162
224
  | Path | Contents |
163
225
  |---|---|
164
- | `src/runtime/components/` | Custom components (`Diff`, `Mermaid`, `FileTree`), one self-describing file each, collected automatically |
165
- | `src/runtime/stock.js` | Which stock OpenUI components ship, their prompt groups, renderer overrides |
166
- | `src/runtime/` | Library assembly, annotation layer, review bridge, styles |
167
- | `src/chrome/` | Review UI around the artifact iframe |
168
- | `src/cli/` | CLI, server, session store, `check`, `export` |
169
- | `src/shared/statements.js` | Statement splitter used by both sides |
170
-
171
- To add or change a component, follow [`.agents/skills/add-component/SKILL.md`](.agents/skills/add-component/SKILL.md). It is a maintainer-only skill and is not published; agents working in this repo pick it up through `AGENTS.md`. Tests enforce the mechanical rules:
172
-
173
- - every component sits in a prompt group
174
- - the component reference stays within its token budget
175
- - released prop orders are append-only (`npm run signatures`)
176
- - every example passes `check`
177
-
178
- For visual checks, `npm run gallery` and `npm run shot` render the components headlessly. `npm run test:browser` drives the real review loop in headless Chrome. It needs Chrome, or `CHROME_PATH` set to a browser, and it checks:
179
-
180
- - every component and example renders at desktop and phone width without errors or sideways scrolling
181
- - every clickable part reaches `poll` correctly
182
-
183
- Before committing, [`.agents/skills/review-change`](.agents/skills/review-change/SKILL.md) runs these checks plus three review agents from `.agents/agents/`:
184
-
185
- - **change-reviewer:** an independent code review
186
- - **visual-qa:** looks at screenshots, including stress inputs
187
- - **cold-start-author:** writes a page from `--help` and `spec` alone, to test the agent-facing docs
226
+ | `src/runtime/components/` | Custom components (`Architecture` family, `Diff`, `FileTree`), one file each |
227
+ | `src/runtime/stock.js` | Which stock components ship, which stay out of `spec`, their groups |
228
+ | `packs/` | Packs bundled with austere-axi (`mermaid`) |
229
+ | `src/runtime/pack-api.js`, `src/pack-kit/` | `austere-axi/pack` and the `components build` pipeline |
230
+ | `src/shared/catalog.js` | Component catalog and prompt tiers |
231
+ | `src/runtime/` | Library, annotation layer, review bridge, styles |
232
+ | `src/chrome/` | Review UI around the page iframe |
233
+ | `src/cli/` | CLI, server, sessions, `check`, `export`, packs, hub |
234
+ | `skills/` | Published skills: `austere`, `austere-setup` |
188
235
 
189
- All of this is maintainer-only and is not published.
236
+ Maintainer guides (not published): [`AGENTS.md`](AGENTS.md), [`.agents/skills/add-component`](.agents/skills/add-component/SKILL.md) to add a component, [`.agents/skills/review-change`](.agents/skills/review-change/SKILL.md) before you commit. Tests enforce the component rules: every component has a group, `spec` stays in its token budget, released prop orders are append-only (`npm run signatures`), and every example passes `check`.
190
237
 
191
- CI (`.github/workflows/ci.yml`) builds and tests on every push to `main` and every pull request: Node 20, 22 and 24 on Linux, Node 24 on Windows, plus the browser suite. To release, bump `version` in `package.json`, push, and publish a GitHub Release. `.github/workflows/publish.yml` then tests again and publishes to npmjs.org through npm trusted publishing, with no token secret.
238
+ Visual checks: `npm run gallery`, `npm run shot`, and `npm run test:browser` (real review loop in headless Chrome; needs Chrome or `CHROME_PATH`).
192
239
 
193
- The runtime is about 3 MB minified. It is served locally and cached. `export` inlines it, and inlines Mermaid too when the artifact uses it.
240
+ **CI** builds and tests every push to `main` and every pull request (Node 20, 22, 24 on Linux; Node 24 on Windows; browser suite). **Release:** bump `version` in `package.json`, push, and publish a GitHub Release. `publish.yml` tests again and publishes to npm with trusted publishing.
194
241
 
195
242
  ## License
196
243
 
197
- MIT. Builds on [OpenUI](https://github.com/thesysdev/openui) (MIT) and borrows the review-loop design of [lavish-axi](https://github.com/kunchenguid/lavish-axi) (MIT).
244
+ MIT. Builds on [OpenUI](https://github.com/thesysdev/openui) (MIT). The review loop design comes from [lavish-axi](https://github.com/kunchenguid/lavish-axi) (MIT).