@satanwagen/reviewkit 0.1.2 → 0.1.4

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/README.md +158 -64
  3. package/dist/activate.cjs +150 -0
  4. package/dist/activate.cjs.map +1 -0
  5. package/dist/activate.d.cts +59 -0
  6. package/dist/activate.d.ts +59 -0
  7. package/dist/activate.js +21 -0
  8. package/dist/activate.js.map +1 -0
  9. package/dist/{chunk-YWBFAV57.js → chunk-5MAFDRUI.js} +1161 -1555
  10. package/dist/chunk-5MAFDRUI.js.map +1 -0
  11. package/dist/{chunk-4YNLMSCK.js → chunk-DG6O5XIC.js} +36 -10
  12. package/dist/chunk-DG6O5XIC.js.map +1 -0
  13. package/dist/chunk-EOX3UJNP.js +36 -0
  14. package/dist/chunk-EOX3UJNP.js.map +1 -0
  15. package/dist/chunk-ETAGGIDN.js +119 -0
  16. package/dist/chunk-ETAGGIDN.js.map +1 -0
  17. package/dist/client/index.cjs +4834 -4244
  18. package/dist/client/index.cjs.map +1 -1
  19. package/dist/client/index.d.cts +1 -1
  20. package/dist/client/index.d.ts +1 -1
  21. package/dist/client/index.js +49 -3
  22. package/dist/client/index.js.map +1 -1
  23. package/dist/effects-EWKHKUDP.js +851 -0
  24. package/dist/effects-EWKHKUDP.js.map +1 -0
  25. package/dist/index-CWGkg1-E.d.cts +86 -0
  26. package/dist/index-DPsjkvaY.d.ts +86 -0
  27. package/dist/index.cjs +4852 -4267
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.js +35 -3
  32. package/dist/index.js.map +1 -1
  33. package/dist/lazy.cjs +9308 -0
  34. package/dist/lazy.cjs.map +1 -0
  35. package/dist/lazy.d.cts +7 -0
  36. package/dist/lazy.d.ts +7 -0
  37. package/dist/lazy.js +8 -0
  38. package/dist/lazy.js.map +1 -0
  39. package/dist/next.cjs +9309 -0
  40. package/dist/next.cjs.map +1 -0
  41. package/dist/next.d.cts +4 -0
  42. package/dist/next.d.ts +4 -0
  43. package/dist/next.js +9 -0
  44. package/dist/next.js.map +1 -0
  45. package/dist/schema.cjs +53 -16
  46. package/dist/schema.cjs.map +1 -1
  47. package/dist/schema.d.cts +20 -5
  48. package/dist/schema.d.ts +20 -5
  49. package/dist/schema.js +30 -2
  50. package/dist/schema.js.map +1 -1
  51. package/package.json +23 -8
  52. package/cli/emblema-sync.mjs +0 -675
  53. package/dist/chunk-4YNLMSCK.js.map +0 -1
  54. package/dist/chunk-YWBFAV57.js.map +0 -1
  55. package/dist/index-BbucFgZi.d.ts +0 -49
  56. package/dist/index-CBlJrKkm.d.cts +0 -49
package/CHANGELOG.md ADDED
@@ -0,0 +1,158 @@
1
+ # Changelog
2
+
3
+ ## 0.1.4 — 2026-09-02
4
+
5
+ ### Added
6
+
7
+ - **`reviewkit` terminal CLI** (in `@satanwagen/reviewkit-cli`, zero deps,
8
+ Node 21+): `cd repo && reviewkit` opens an interactive live view of every
9
+ review item across the site's pages: presence (who is online, in their
10
+ colours), arrow-key navigation, Enter for actions (detail, apply with
11
+ Claude Code, copy prompt, set status, delete), `a` to hand all open items
12
+ to Claude Code, filters, live toasts for new / changed / deleted items.
13
+ Piped output falls back to a plain table plus event stream. `list`, `show`,
14
+ `status`, `delete`, `watch`, `export`, `prompt`, `rooms`, `projects`, `key`.
15
+ Project resolution: `-p name` → `.reviewkit/project.json` (walks up from
16
+ cwd) → `.reviewkit/emblema-sync.json` → global active project.
17
+ - **One-click pairing.** `reviewkit setup` detects the site's sync settings in
18
+ the repo, listens on loopback (127.0.0.1:48780–48783) and the review panel's
19
+ new **Connect terminal / agent…** dialog finds it and pushes the connection.
20
+ Nothing to copy. A copyable `rkc_…` key remains for remote machines
21
+ (`reviewkit connect <key>`). CSP: the panel needs `connect-src
22
+ http://127.0.0.1:*` for this (already required by the Emblema bridge).
23
+ - **MCP bridge for coding agents.** `reviewkit mcp` is a stdio MCP server
24
+ with `reviewkit_prompt`, `reviewkit_list`, `reviewkit_show`,
25
+ `reviewkit_set_status`, `reviewkit_delete`, `reviewkit_export`,
26
+ `reviewkit_rooms`, `reviewkit_projects`. `setup` / `mcp-install` register it
27
+ in Claude Code (`claude mcp add`) and Cursor (`~/.cursor/mcp.json`).
28
+ Statuses set by the agent appear live in reviewers' browsers.
29
+ - **Sync server: `rooms` request** (protocol v1, additive). Returns the routes
30
+ of the connection's origin that have review data, live or on disk, with item
31
+ counts and online peers. Needed by the CLI. Deployed on the satanwagen
32
+ server, reachable again at `wss://2-59-219-26.sslip.io/rk-sync` (sslip.io
33
+ hostname, no domain to expire) — and the client's default `syncUrl` again.
34
+ - Panel gear menu: **Connect terminal / agent…** (enabled when live sync is on).
35
+
36
+ ### Changed
37
+
38
+ - **One word does everything: `<ReviewKit token="…" />`.** Live collaboration
39
+ is on by default: `syncUrl` defaults to the public ReviewKit server
40
+ `wss://2-59-219-26.sslip.io/rk-sync` (sslip.io hostname → VPS IP, nothing to
41
+ expire) and `syncToken` defaults to `token`. The server is now
42
+ **self-serve**: any token of at least 8 characters gets its own private set
43
+ of rooms (keyed by origin + route + hash(token)), so no account and no
44
+ server-side step exist. New prop `sync={false}` forces solo mode;
45
+ `syncToken` stays as an override when the room secret should differ from
46
+ the review link. Existing installs that set `token` but no `syncToken`
47
+ start syncing on upgrade — pass `sync={false}` to keep the old solo mode.
48
+ Same in the CLI: `init --sync-url` is optional, state files still carrying
49
+ the retired host fall back to the default with a warning, and `reviewkit
50
+ setup` picks up the review token from the repo as the room secret.
51
+ - `@satanwagen/reviewkit-cli` now ships two tools. `npx -y
52
+ @satanwagen/reviewkit-cli …` runs `reviewkit`; the Emblema agent is
53
+ `npx -p @satanwagen/reviewkit-cli reviewkit-emblema …` (docs and in-app
54
+ hints updated).
55
+
56
+ ## 0.1.3 — 2026-09-02
57
+
58
+ Defaults tightened so a production build behaves the way its code reads.
59
+ Several changes are breaking for hosts that relied on the old conveniences;
60
+ each has a one-line migration below.
61
+
62
+ ### Breaking
63
+
64
+ - **No activation without a token.** Previously an unset `token` meant "any
65
+ `?review=x` turns the layer on" (plus the `reviewkit` localStorage flag).
66
+ Now, without `token`, nothing activates. Migration: pass `token`. For a
67
+ reviewer-less dev loop pass `devAutoOn`.
68
+ - **No `NODE_ENV === 'development'` auto-on.** The library never reads the
69
+ host's NODE_ENV any more. Migration:
70
+ `devAutoOn={import.meta.env.DEV}` (Vite) or
71
+ `devAutoOn={process.env.NODE_ENV === 'development'}` (Next/webpack).
72
+ - **No default `syncUrl`.** The former default
73
+ `wss://agropolio.fucking.style/rk-sync` pointed at an expired domain. Sync
74
+ now runs only when **both** `syncUrl` and `syncToken` are set; otherwise
75
+ the layer is local-only, no socket. Migration: set `syncUrl` explicitly.
76
+ Same for the CLI: `reviewkit-emblema init --sync-url …` (the room bridge
77
+ stays disabled until both `--sync-url` and `--sync-token` are configured).
78
+ - **CLI moved to `@satanwagen/reviewkit-cli`.** The main package no longer
79
+ ships a `bin`, so a host `npm install` does not pull the Emblema agent.
80
+ Migration: `npx -p @satanwagen/reviewkit-cli reviewkit-emblema init …` / `serve` (the command
81
+ inside is still `reviewkit-emblema`).
82
+ - **`emblema.repoPath` is optional.** The repo path is Emblema's local
83
+ knowledge (advertised in its `/intake/ping` answer, and present in
84
+ `.reviewkit/emblema-sync.json` for the agent) and no longer belongs in host
85
+ code. `emblema={{}}` now enables the bridge. The v1 dispatch payload keeps
86
+ `v: 1`, but `repoPath` became optional: an intake with a strict "required"
87
+ validator must relax it and fall back to its active project.
88
+ - **Effect sounds moved off the expired domain.** `/ondro` now fetches
89
+ `rick.mp3` from `https://2-59-219-26.sslip.io/rk-assets` (the same VPS,
90
+ reached through a sslip.io hostname so no domain renewal can break it).
91
+ New prop `fxAssetsUrl` overrides the base for self-hosting. Add the origin
92
+ to CSP `media-src` if you send a policy.
93
+ - Legacy activation keys (`review-kit:active`, `reviewkit`) are still cleaned
94
+ up on load but never consulted.
95
+ - **Emblema intake must accept a batch without `repoPath`.** An Emblema build
96
+ that still validates it as required answers HTTP 400; the panel now shows
97
+ that as `emblema rejected` (new push status) instead of `emblema offline`.
98
+ - **CLI:** a `.reviewkit/emblema-sync.json` written by 0.1.2 still carries the
99
+ retired default sync host. `serve` now detects it, logs a re-init hint and
100
+ runs with the bridge disabled instead of reconnecting forever; the local
101
+ `/status` endpoint reports `realtime.state: 'off'` whenever either sync
102
+ setting is missing (previously it said `connecting` with no URL).
103
+
104
+ ### Added
105
+
106
+ - `enabled?: boolean` master switch (default `true`) so a wrapper can write
107
+ `<ReviewKit enabled={Boolean(TOKEN)} token={TOKEN} />` instead of
108
+ `if (!TOKEN) return null`.
109
+ - `devAutoOn?: boolean` — host-controlled "always on for this load". Still
110
+ subject to `enabled`, `allowedHosts` and `?review=off`.
111
+ - **`@satanwagen/reviewkit/next`** — `'use client'` component: render it from
112
+ a server `layout.tsx`; it evaluates URL + sticky session after mount and
113
+ only then dynamically imports the core. `examples/next-demo` shows it
114
+ together with the CSP headers.
115
+ - **`@satanwagen/reviewkit/lazy`** — the same code-split component for Vite
116
+ and friends; no more hand-rolled `React.lazy` + `Suspense` gate.
117
+ - **`@satanwagen/reviewkit/activate`** — `shouldActivate({ token, enabled,
118
+ allowedHosts, devAutoOn })`, React-free, ~200 B. The supported way for a
119
+ host to gate its own lazy import; the sessionStorage format stays private.
120
+ - One-time console warning when a URL token activates the layer on a
121
+ non-loopback hostname with no `allowedHosts` (see README "Production
122
+ recipe").
123
+ - The `/` command effects (~40 KB of source) load as a separate chunk on
124
+ first use instead of shipping in the core chunk.
125
+ - `probeEmblema()` returns `{ port, repoPath? }`; `pingEmblema()` stays as a
126
+ port-only wrapper.
127
+ - Activation matrix tests (`test/activate.test.mjs`) run against `dist/`
128
+ (`npm test` builds first via `pretest`).
129
+ - A one-time console warning when `?review=<x>` does not match the configured
130
+ token (kept from 0.1.2, now also emitted by `shouldActivate`).
131
+ - `/lazy` and `/next` are effectively ESM entries: the CJS builds exist for
132
+ `require()` consumers but inline the core (esbuild has no CJS code
133
+ splitting). Every modern bundler picks the ESM build.
134
+ - README: activation-state table, CSP requirements, production recipe.
135
+
136
+ ### Bundle audit (unminified ESM, `dist/`)
137
+
138
+ | Chunk | 0.1.2 | 0.1.3 |
139
+ | --- | ---: | ---: |
140
+ | core client chunk | 313 KB | 267 KB |
141
+ | effects (lazy, on first `/command`) | in core | 41 KB |
142
+ | `activate` entry | — | 0.2 KB |
143
+
144
+ Largest remaining inputs: `styles.ts` (53 KB), `Panel.tsx` (46 KB). Schema
145
+ validation and IndexedDB persistence are under 10 KB each and were not the
146
+ weight.
147
+
148
+ ## 0.1.2
149
+
150
+ - Delete endpoint for Emblema findings, idempotent for unknown ids.
151
+
152
+ ## 0.1.1
153
+
154
+ - `reopen` status verb; realtime health in local state.
155
+
156
+ ## 0.1.0
157
+
158
+ - First scoped release as `@satanwagen/reviewkit`.
package/README.md CHANGED
@@ -13,35 +13,104 @@ npm install @satanwagen/reviewkit
13
13
  ## Two-line integration
14
14
 
15
15
  ```tsx
16
- import { ReviewKit } from '@satanwagen/reviewkit/client';
16
+ import { ReviewKit } from '@satanwagen/reviewkit/lazy'; // Vite, CRA, Remix …
17
+ // import { ReviewKit } from '@satanwagen/reviewkit/next'; // Next.js App Router ('use client' built in)
17
18
 
18
19
  // anywhere in your tree, rendered once:
19
- <ReviewKit />
20
+ <ReviewKit token="pick-any-word" />
20
21
  ```
21
22
 
22
- That's it. The component renders `null` unless review mode is active, and all UI lives in a shadow root appended to `document.body` — your CSS never leaks in, reviewkit's CSS never leaks out. Safe to ship in production builds.
23
+ That's it — one word. Reviewers open `https://your-site/?review=pick-any-word`; the same word is also the secret of your private rooms on the public ReviewKit sync server, so presence, live cursors, shared items, the terminal CLI and the Claude Code bridge all work with no server-side step. The component renders `null` unless review mode is active, and all UI lives in a shadow root appended to `document.body` — your CSS never leaks in, reviewkit's CSS never leaks out. Safe to ship in production builds. Want it offline? `sync={false}`.
24
+
25
+ Three entries, same props:
26
+
27
+ | Entry | What it is | Use when |
28
+ | --- | --- | --- |
29
+ | `@satanwagen/reviewkit/lazy` | Component that decides after mount (URL + sticky session) and only then `import()`s the core chunk. | Vite / CRA / Remix / any client bundler. Visitors without a review link never download the core. |
30
+ | `@satanwagen/reviewkit/next` | Same, with `'use client'` baked into the built file so a server `layout.tsx` can render it directly. | Next.js App Router. See `examples/next-demo`. |
31
+ | `@satanwagen/reviewkit/client` | The core itself, eager. | You already have your own lazy boundary, or you want everything in one chunk. |
32
+
33
+ The code split only exists in the ESM builds (what Vite, Next, webpack 5 pick); the CJS files of `/lazy` and `/next` inline the core for `require()` consumers.
34
+
35
+ And a React-free helper for hosts that gate their own lazy import:
36
+
37
+ ```ts
38
+ import { shouldActivate } from '@satanwagen/reviewkit/activate'; // ~200 B, no React, SSR-safe (false)
39
+ if (shouldActivate({ token: TOKEN })) void import('./reviewHost');
40
+ ```
41
+
42
+ `shouldActivate` is the supported API for "should this page load show review?"; the sessionStorage format behind the sticky session is private and may change. Its only side effect: `?review=off` clears the sticky opt-in.
23
43
 
24
44
  ### Props
25
45
 
26
46
  | Prop | Type | Description |
27
47
  | --- | --- | --- |
28
- | `token` | `string` | Restricts URL activation to `?review=<token>`. Without it, any `?review=x` activates (with a console warning). |
29
- | `syncToken` | `string` | Enables live collaboration via the realtime sync server (`realtime/`): presence avatars, live cursors, remote selections and instantly shared items. Unset = solo mode, no network. The value is public in the browser bundle by nature — treat it as a gate against casual visitors, not a secret. Keep it in the lazy review chunk of the host app, never in the main bundle. |
48
+ | `token` | `string` | **Required for any activation.** The layer turns on only via `?review=<token>` (or the per-tab session such a link starts). Without it nothing activates, whatever the URL says. |
49
+ | `enabled` | `boolean` | Master switch, default `true`. `false` renders nothing, installs nothing. Lets you write `enabled={Boolean(TOKEN)}` instead of `if (!TOKEN) return null`. |
50
+ | `devAutoOn` | `boolean` | Force the layer on for this page load, no link needed. Host-controlled dev convenience (`devAutoOn={import.meta.env.DEV}`); the library never reads `NODE_ENV`. Still subject to `enabled`, `allowedHosts` and `?review=off`. |
51
+ | `allowedHosts` | `string[]` | Hostname allowlist with `*` wildcards (`['localhost', '*.staging.example']`). Outside it nothing activates, token or not. Unset = any host (with a one-time console warning on URL activation on a public host). |
52
+ | `syncUrl` | `string` | WebSocket endpoint of the sync server. Default: the public ReviewKit server `wss://2-59-219-26.sslip.io/rk-sync`, so live collaboration needs no server of your own. Override for a self-hosted `realtime/server`. |
53
+ | `sync` | `boolean` | Live collaboration switch, default `true`. `false` = solo mode, no network. |
54
+ | `syncToken` | `string` | Secret of your rooms on the sync server, **defaults to `token`**. Any string of 8+ characters gets its own private rooms on the public server. Override only if the room secret should differ from the review link. Public in the bundle by nature. |
55
+ | `emblema` | `{ repoPath?: string; port?: number }` | Enables the Emblema bridge (`emblema={{}}` is enough). See "Pairing with Emblema". |
30
56
  | `defaultAuthor` | `string` | Pre-fills the reviewer name. |
57
+ | `fxAssetsUrl` | `string` | Base URL for the `/` effect soundtracks (`<base>/rick.mp3` for `/ondro`). Default: the public ReviewKit assets host `https://2-59-219-26.sslip.io/rk-assets`. Override to self-host. Needs `media-src` in CSP. |
31
58
  | `position` | `'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right'` | Initial corner of the floating badge (default `bottom-right`; draggable, persisted). |
32
59
  | `storageKey` | `string` | Persistence key (default: page origin). |
33
60
  | `onSessionChange` | `(session: ReviewSession) => void` | Called (debounced) whenever the session changes. |
34
61
 
35
62
  ## How the layer activates
36
63
 
37
- **With a `token` configured (the production setup) the rules are strict:**
64
+ Rules, in order; the first that applies wins. No environment sniffing anywhere.
65
+
66
+ | # | Condition | Result |
67
+ | --- | --- | --- |
68
+ | 1 | `enabled={false}` | off |
69
+ | 2 | hostname not in `allowedHosts` (when given) | off |
70
+ | 3 | `?review=off` in the URL | off, and the in-tab sticky session is cleared |
71
+ | 4 | `devAutoOn` | **on** |
72
+ | 5 | no `token` configured | off (`?review=x` logs a one-time warning and stays off) |
73
+ | 6 | `?review=<token>` matches | **on**, starts the per-tab sticky session |
74
+ | 7 | `?review=<something else>` | off |
75
+ | 8 | valid sticky session (same token, < 4 h since last active load) | **on**, expiry renewed |
76
+ | 9 | otherwise | off |
77
+
78
+ Activation-state matrix for the common cases:
79
+
80
+ | token | build | URL | sticky | result |
81
+ | --- | --- | --- | --- | --- |
82
+ | unset | dev or prod | none / `?review=x` | any | off |
83
+ | unset | dev, with `devAutoOn` | any except `off` | any | on |
84
+ | set | prod | `?review=<token>` | — | on (sticky starts) |
85
+ | set | prod | none | valid | on |
86
+ | set | prod | none | none / expired / other token | off |
87
+ | set | prod | `?review=wrong` | valid | off |
88
+ | set | any | `?review=off` | valid | off, sticky cleared |
89
+
90
+ The sticky session is per tab (`sessionStorage`), bound to the token and renewed on every active load, so a reviewer who opened a review link keeps reviewing across full page loads on other routes. The gear menu's **Keep review on in this tab** toggles it explicitly; **End review** (or `?review=off`) clears everything and returns the clean site. Leftover keys from pre-0.1.3 versions (`review-kit:active`, `reviewkit`) are removed on load and never consulted.
91
+
92
+ ### Production recipe
93
+
94
+ ```tsx
95
+ <ReviewKit
96
+ enabled={Boolean(TOKEN)}
97
+ token={TOKEN}
98
+ allowedHosts={['staging.your-site.tld', '*.preview.your-site.tld']}
99
+ />
100
+ ```
101
+
102
+ `token` + `allowedHosts` together: the token says *who*, the allowlist says *where*. Without `allowedHosts` a URL activation on a non-loopback host logs one console warning. `TOKEN` is public in the bundle by design; use the `/lazy` or `/next` entry so it at least lives in the review chunk, not the main one.
103
+
104
+ ### CSP
105
+
106
+ If the host sends a `Content-Security-Policy`, `connect-src` must allow:
38
107
 
39
- 1. `?review=<token>` in the URL the only automatic way in.
40
- 2. The reviewer's explicit **“Keep on in this tab”** toggle in the panel — a per-tab, token-bound opt-in that expires after ~4 hours. Nothing persists silently.
108
+ - the sync server origin: `https://2-59-219-26.sslip.io` for the default server (or your own `syncUrl` origin / `ws:` in dev),
109
+ - `http://127.0.0.1:*` when the `emblema` prop is set (intake ports 48752–48755, pairing agent 48770–48773).
41
110
 
42
- Anything else shows nothing: no badge, no listeners, no WebSocket. **“End review”** in the panel (or `?review=off`) clears all persisted state and returns the clean site. Leftover activation keys from pre-strict versions (`review-kit:active`, `reviewkit`) are silently removed on the first token-less load.
111
+ and `media-src` must allow `https://2-59-219-26.sslip.io` (or your `fxAssetsUrl`) for the `/` effect sounds; without it the effects run silent.
43
112
 
44
- **Without a token** (zero-config/dev) the conveniences remain: any `?review=x` value (console warning), the in-tab opt-in, a `reviewkit` localStorage flag, or a dev build (`NODE_ENV === 'development'`).
113
+ With `connect-src 'self'` alone the WebSocket is blocked and the layer silently stays local-only. `examples/next-demo/next.config.ts` has a complete header.
45
114
 
46
115
  ## Using it (reviewer flow)
47
116
 
@@ -121,70 +190,64 @@ Programmatic helpers are exported too: `parseSessionJson` / `parseSession` (vali
121
190
  // src/main.tsx
122
191
  import { StrictMode } from 'react';
123
192
  import { createRoot } from 'react-dom/client';
124
- import { ReviewKit } from '@satanwagen/reviewkit/client';
193
+ import { ReviewKit } from '@satanwagen/reviewkit/lazy';
125
194
  import { App } from './App';
126
195
 
127
196
  createRoot(document.getElementById('root')!).render(
128
197
  <StrictMode>
129
198
  <App />
130
- <ReviewKit token={import.meta.env.VITE_REVIEW_TOKEN} />
199
+ <ReviewKit token={import.meta.env.VITE_REVIEW_TOKEN} devAutoOn={import.meta.env.DEV} />
131
200
  </StrictMode>,
132
201
  );
133
202
  ```
134
203
 
135
- No Vite config changes are needed — `reviewkit` ships prebuilt ESM/CJS with types. In dev the layer is always on; in production reviewers open `https://your-site.tld/?review=<token>`.
204
+ No Vite config changes are needed — `reviewkit` ships prebuilt ESM/CJS with types. In `vite dev` the layer is on thanks to `devAutoOn`; in production reviewers open `https://your-site.tld/?review=<token>`, and visitors without the link never download the review chunk (the `/lazy` entry splits it automatically).
136
205
 
137
- Until the npm release lands you can also depend on it via a file path — this is exactly what the demo does:
206
+ Until the npm release lands you can also depend on it via a file path — this is exactly what the demos do:
138
207
 
139
208
  ```jsonc
140
209
  // package.json
141
210
  "dependencies": { "@satanwagen/reviewkit": "file:../path/to/ReviewKit" }
142
211
  ```
143
212
 
144
- npm symlinks the folder, so after editing reviewkit run `npm run build` in it and restart/reload Vite. Note that the deployed site only picks up library changes after a rebuild + deploy of the host app (Fable has `npm run deploy` for the whole chain) — the live sync server only carries review data, never code. Run `npm run build` in `ReviewKit/` at least once before installing, since the package resolves to `dist/`.
213
+ npm symlinks the folder, so after editing reviewkit run `npm run build` in it and restart/reload Vite. With React 18 add `resolve: { dedupe: ['react', 'react-dom'] }` to `vite.config.ts` so the linked package's own `node_modules` cannot introduce a second React. Run `npm run build` in `ReviewKit/` at least once before installing, since the package resolves to `dist/`.
145
214
 
146
- ### Real-world example: token-only activation + lazy loading (Agropolio landing page)
215
+ ### Gating a hand-rolled lazy boundary
147
216
 
148
- The Agropolio landing page (`../Agropolio/Fable` relative to this repo) integrates reviewkit so that **only** `?review=agropolio` ever activates it dev-build auto-activation is bypassed, and without the token the library code isn't even downloaded (separate lazy chunk):
217
+ If you keep your own wrapper (for example to keep `syncToken` in a chunk you control), gate it with `shouldActivate` instead of reading ReviewKit's storage keys:
149
218
 
150
- ```jsonc
151
- // Fable/package.json
152
- "dependencies": { "@satanwagen/reviewkit": "file:../../ReviewKit" }
153
- ```
219
+ ```tsx
220
+ // ReviewLayer.tsx
221
+ import { lazy, Suspense, useEffect, useState } from 'react';
222
+ import { shouldActivate } from '@satanwagen/reviewkit/activate';
154
223
 
155
- ```ts
156
- // Fable/vite.config.ts the linked package has its own node_modules,
157
- // dedupe guarantees a single React instance (Fable is on React 18):
158
- resolve: { dedupe: ['react', 'react-dom'] }
159
- ```
224
+ const TOKEN = import.meta.env.VITE_REVIEW_TOKEN;
225
+ const ReviewHost = lazy(() => import('./reviewHost')); // renders <ReviewKit token={TOKEN} syncUrl=… syncToken=… />
160
226
 
161
- ```tsx
162
- // Fable/src/ReviewLayer.tsx (rendered next to <App /> in main.tsx)
163
- import { lazy, Suspense, useEffect, useState } from 'react'
164
-
165
- const TOKEN = 'agropolio'
166
- const SESSION_KEY = 'review-kit:active' // same key reviewkit uses internally
167
-
168
- function shouldLoad(): boolean {
169
- try {
170
- const param = new URLSearchParams(window.location.search).get('review')
171
- if (param === 'off') { window.sessionStorage.removeItem(SESSION_KEY); return false }
172
- if (param === TOKEN) return true
173
- return window.sessionStorage.getItem(SESSION_KEY) === '1' // survive reloads in-tab
174
- } catch { return false }
227
+ export function ReviewLayer() {
228
+ const [load, setLoad] = useState(false);
229
+ useEffect(() => { if (shouldActivate({ token: TOKEN })) setLoad(true); }, []);
230
+ return load ? <Suspense fallback={null}><ReviewHost /></Suspense> : null;
175
231
  }
232
+ ```
176
233
 
177
- const ReviewKit = lazy(() => import('@satanwagen/reviewkit/client').then((m) => ({ default: m.ReviewKit })))
234
+ ## Integrating into a Next.js site (App Router)
178
235
 
179
- export function ReviewLayer() {
180
- const [load, setLoad] = useState(false)
181
- useEffect(() => { if (shouldLoad()) setLoad(true) }, [])
182
- if (!load) return null
183
- return <Suspense fallback={null}><ReviewKit token={TOKEN} /></Suspense>
236
+ ```tsx
237
+ // app/layout.tsx (a server component — that's fine)
238
+ import { ReviewKit } from '@satanwagen/reviewkit/next';
239
+
240
+ export default function RootLayout({ children }) {
241
+ return (
242
+ <html><body>
243
+ {children}
244
+ <ReviewKit token={process.env.NEXT_PUBLIC_REVIEW_TOKEN} devAutoOn={process.env.NODE_ENV === 'development'} />
245
+ </body></html>
246
+ );
184
247
  }
185
248
  ```
186
249
 
187
- The gate outside `<ReviewKit />` is what keeps the layer invisible to normal visitors: no token the wrapper renders `null`, no shadow root, no listeners, no network request for the reviewkit chunk.
250
+ The `/next` entry is a client component; it renders nothing on the server and evaluates the URL/sticky session after mount before importing the core. Add the CSP `connect-src` entries above if you send a policy. Full example: `examples/next-demo`.
188
251
 
189
252
  ## Adding ReviewKit to another site (with live collaboration)
190
253
 
@@ -203,19 +266,19 @@ Step by step, for any site on any hosting (nginx, IIS, Vercel, …).
203
266
 
204
267
  Run `npm run build` inside ReviewKit at least once for the `file:`/git variants (the package resolves to `dist/`). With React 18 add `resolve.dedupe: ['react','react-dom']` to your bundler config when using a `file:` link.
205
268
 
206
- **2. Integrate — two lines, done. Live collaboration included:**
269
+ **2. Integrate — one line, done:**
207
270
 
208
271
  ```tsx
209
- import { ReviewKit } from '@satanwagen/reviewkit/client';
272
+ import { ReviewKit } from '@satanwagen/reviewkit/lazy';
210
273
 
211
- <ReviewKit token="my-site-token" syncToken="<the sync server's shared secret>" />
274
+ <ReviewKit token="my-site-token" />
212
275
  ```
213
276
 
214
- That's the whole setup. `syncUrl` **defaults to the always-on ReviewKit sync server** (`wss://agropolio.fucking.style/rk-sync`), so presence, live cursors and instantly-shared items work on any site with no server-side step at all no allowlist entry, no restart, nothing. Rooms are keyed by the page's origin + path, so different sites never see each other's feedback or presence. If the server is unreachable the tool silently runs local-only (no console errors); omit `syncToken` to disable networking entirely.
277
+ Live collaboration (presence, live cursors, instantly shared items) is on by default: `syncUrl` points at the public ReviewKit server `wss://2-59-219-26.sslip.io/rk-sync` and the room secret is your `token` (8+ characters; `syncToken` overrides it). The server is self-serve: every token gets its own private rooms, no account, no registration. Rooms are keyed by the page's origin + path, so different sites never see each other's feedback or presence. If the server is unreachable the tool silently runs local-only (no console errors); omit either prop to disable networking entirely.
215
278
 
216
- **What the open server implies (honesty section):** the token and `syncToken` are readable in the browser bundle, so anyone holding a review URL can join and create rooms. The server is meant for the operator's own sites, not as a public service; it is guarded by the shared token, per-connection rate limits (40 msg/s, burst 80), 1 MiB frame cap, 16 clients and 2000 items per room, and a live-room ceiling (`RK_SYNC_MAX_ROOMS`, default 500).
279
+ **What a shared server implies (honesty section):** the token is readable in the browser bundle and in every review link, so anyone holding the link can join your rooms that is the point of a review link. Rooms of different tokens never mix. The public server is guarded by per-connection rate limits (40 msg/s, burst 80), 1 MiB frame cap, 16 clients and 2000 items per room, and a live-room ceiling (`RK_SYNC_MAX_ROOMS`, default 500).
217
280
 
218
- **3. Advanced: run your own sync server** (optional) — `realtime/server/`: `npm run build`, run `dist/server.js` with `RK_SYNC_TOKEN` (+ optional `RK_SYNC_PORT`, `RK_SYNC_DATA`, `RK_SYNC_MAX_ROOMS`, and `RK_SYNC_ORIGINS` if you *want* an origin allowlist — empty/unset means any origin, which is the default deployment) as a service, point `syncUrl` at it, then put a WebSocket-aware reverse proxy in front:
281
+ **3. Run the sync server** — `realtime/server/`: `npm run build`, run `dist/server.js` with `RK_SYNC_TOKEN` (+ optional `RK_SYNC_PORT`, `RK_SYNC_DATA`, `RK_SYNC_MAX_ROOMS`, and `RK_SYNC_ORIGINS` if you *want* an origin allowlist — empty/unset means any origin, which is the default deployment) as a service, point `syncUrl` at it, then put a WebSocket-aware reverse proxy in front:
219
282
 
220
283
  *nginx* (as deployed on satan):
221
284
 
@@ -233,23 +296,49 @@ That's the whole setup. `syncUrl` **defaults to the always-on ReviewKit sync ser
233
296
 
234
297
  With an `RK_SYNC_ORIGINS` allowlist active, rejected origins are logged (`[origin] rejected …`) and closed with code 4005; the client quietly stays local.
235
298
 
236
- **4. Token caveat:** don't reuse secrets that protect anything else, and keep tokens out of the main bundle (load ReviewKit in a lazy chunk like the Agropolio integration does).
299
+ **4. Token caveat:** don't reuse secrets that protect anything else, and keep tokens out of the main bundle (use the `/lazy` or `/next` entry). Add `allowedHosts` in production (see "Production recipe").
237
300
 
238
301
  **5. Verify it works:** open `https://your-site/?review=<token>` — the Review badge appears; the panel header shows **live** (green dot) when the sync server accepted you, or *local only* when not. In the Network tab you should see one WebSocket to `/rk-sync` with a `welcome` frame. Open the same URL in a second browser: avatars, live cursors and instantly-appearing items confirm collaboration.
239
302
 
303
+ ## Terminal & coding agents (`reviewkit` CLI)
304
+
305
+ Review comments do not have to stay in the browser. The `reviewkit` CLI reads the same live rooms, and doubles as the MCP bridge for Claude Code, Cursor and any other MCP client, so an agent can read every comment and mark items applied while reviewers watch the status flip live.
306
+
307
+ ```sh
308
+ npm install -g @satanwagen/reviewkit-cli
309
+ cd my-site
310
+ reviewkit # first run pairs the repo (one click in the review panel), then the live view
311
+ ```
312
+
313
+ **Pairing is one click, no copy-paste.** Bare `reviewkit` in an unpaired repo runs `setup` first; it detects the site's `token` (and a custom `syncToken` / `syncUrl`, if any) in the repo, opens a loopback port and waits. In the browser open the review panel → gear → **Connect terminal / agent…** → **Connect**: the panel finds the waiting terminal and pushes the connection into `<repo>/.reviewkit/project.json` (gitignored). `setup` also registers the MCP server in Claude Code and Cursor when it finds them. The dialog keeps a copyable key as the fallback for a terminal on another machine.
314
+
315
+ The live view is interactive: arrow keys walk the items, Enter opens actions (detail, apply this item with Claude Code, copy as prompt, mark applied / accepted / rejected / reopen, delete), `a` hands every open item to Claude Code in the repo, and the header shows who is reviewing right now in their own colours. Everything is the shared room state, so a status flipped in the terminal or by an agent changes in reviewers' browsers instantly, and vice versa.
316
+
317
+ Everyday commands: `reviewkit list --status open`, `reviewkit show <id>`, `reviewkit status <id> applied`, `reviewkit watch`, `reviewkit export -o review.json`, `reviewkit prompt` (agent-ready work list), `reviewkit rooms`, `reviewkit projects` / `-p <name>` to act on another paired site. Full table in [`cli/README.md`](./cli/README.md).
318
+
319
+ **Agents.** With the bridge registered, "apply the reviewkit feedback" is enough: the agent calls `reviewkit_prompt`, edits code, then `reviewkit_set_status(id, "applied")` per item. Manual registration:
320
+
321
+ ```sh
322
+ claude mcp add --scope user reviewkit -- npx -y @satanwagen/reviewkit-cli mcp
323
+ ```
324
+
325
+ Requirements: the site has live sync on (the default), Node 21+. The CLI reads shared rooms, never a reviewer's local browser storage. The sync server gained a `rooms` request (protocol v1, additive) so the CLI can find every page with review data; run the 0.1.4 server build.
326
+
240
327
  ## Pairing with Emblema
241
328
 
242
329
  ReviewKit pairs with the [Emblema](https://emblema.app) desktop app: once paired, the local sync agent streams review items to Emblema, and the paired Emblema shows them as findings in its ReviewKit tile (statuses flow back to the page).
243
330
 
244
- Sync is automatic and realtime — no manual step. With `emblema` set in the `ReviewKit` config, the browser itself pushes every added or edited open item to a locally running Emblema (debounced 500 ms, silent-fail: an unreachable Emblema never blocks the reviewer; the panel header shows a small `emblema` status dot). The session menu keeps **Resend all to Emblema** as a one-click fallback that re-dispatches every open item on the page.
331
+ Sync is automatic and realtime — no manual step. With `emblema={{}}` in the `ReviewKit` config, the browser itself pushes every added or edited open item to a locally running Emblema (debounced 500 ms, silent-fail: an unreachable Emblema never blocks the reviewer; the panel header shows a small `emblema` status dot). The session menu keeps **Resend all to Emblema** as a one-click fallback that re-dispatches every open item on the page.
332
+
333
+ **Which repo gets edited** is Emblema's knowledge, not the website's: the intake advertises its `repoPath` in the `/intake/ping` answer, and the local agent has it in `.reviewkit/emblema-sync.json` (`init --repo`, default: the directory you ran it in). `emblema.repoPath` remains as an optional override only; an absolute path from a reviewer's laptop has no place in host code.
245
334
 
246
335
  **Where the key lives:** the pairing key and state are stored by the local sync agent in `<project>/.reviewkit/emblema-sync.json`. The key (`emk_…`) is a secret — the panel only ever displays it shortened (`emk_…last4`) and puts the full value on the clipboard.
247
336
 
248
- **Set up and generate a key** (in the project repo):
337
+ **Set up and generate a key** (in the project repo; the CLI is its own package so host installs stay CLI-free):
249
338
 
250
339
  ```sh
251
- npx reviewkit-emblema init --origin https://your-site # creates the state file, prints the key
252
- npx reviewkit-emblema serve # runs the local API + realtime bridge
340
+ npx -p @satanwagen/reviewkit-cli reviewkit-emblema init --origin https://your-site --sync-url wss://your-site/rk-sync --sync-token <secret>
341
+ npx -p @satanwagen/reviewkit-cli reviewkit-emblema serve # local API + realtime bridge (bridge needs both sync flags)
253
342
  ```
254
343
 
255
344
  Paste the printed key into Emblema. From the page UI the same lives in the panel's gear menu → **Connect to Emblema…** — the menu entry shows the live pairing status at a glance (green dot = paired, amber = the local agent isn't answering, red = key revoked, grey = not paired), and the dialog has **New key (rotate)**, **Copy key** and **Delete key (revoke)**.
@@ -257,22 +346,24 @@ Paste the printed key into Emblema. From the page UI the same lives in the panel
257
346
  **Rotate / revoke from the CLI:**
258
347
 
259
348
  ```sh
260
- npx reviewkit-emblema key # print the current key
261
- npx reviewkit-emblema rotate # issue a new secret — the old key stops working immediately
262
- npx reviewkit-emblema revoke # disable the pairing entirely (rotate re-enables)
349
+ npx -p @satanwagen/reviewkit-cli reviewkit-emblema key # print the current key
350
+ npx -p @satanwagen/reviewkit-cli reviewkit-emblema rotate # issue a new secret — the old key stops working immediately
351
+ npx -p @satanwagen/reviewkit-cli reviewkit-emblema revoke # disable the pairing entirely (rotate re-enables)
263
352
  ```
264
353
 
265
354
  Rotate and revoke take effect immediately, including against an already-running `serve` process — authenticated requests always check the current on-disk key, so a rotated or revoked key can never keep authenticating.
266
355
 
267
- ## Demo
356
+ If the host sends a CSP, the bridge needs `connect-src http://127.0.0.1:*` (see "CSP").
357
+
358
+ ## Demos
268
359
 
269
360
  ```sh
270
361
  cd examples/vite-demo
271
362
  npm install
272
- npm run dev # http://localhost:3010 — dev build, so the layer is active immediately
363
+ npm run dev # http://localhost:3010 — devAutoOn keeps the layer on in dev
273
364
  ```
274
365
 
275
- Also try `http://localhost:3010/?review=demo` to exercise URL activation.
366
+ Also try `http://localhost:3010/?review=demo-review` to exercise URL activation. `examples/next-demo` is the same site on Next.js (App Router, `/next` entry, CSP headers) on port 3011.
276
367
 
277
368
  ## Development
278
369
 
@@ -281,4 +372,7 @@ npm install
281
372
  npm run typecheck
282
373
  npm run lint
283
374
  npm run build # tsup → dist/ (ESM + CJS + d.ts)
375
+ npm test # CLI tests + activation matrix (needs dist/)
284
376
  ```
377
+
378
+ See `CHANGELOG.md` for the 0.1.3 default changes and migration notes.