@myrtilleio/redline 0.1.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 +281 -0
- package/bin/redline.js +375 -0
- package/client/boot.js +146 -0
- package/client/check.js +926 -0
- package/client/composer.js +384 -0
- package/client/core.js +1610 -0
- package/client/devices.js +273 -0
- package/client/edits.js +389 -0
- package/client/keyboard.js +110 -0
- package/client/panel.js +222 -0
- package/client/pen.js +324 -0
- package/client/references.js +114 -0
- package/client/select.js +180 -0
- package/client/shots.js +357 -0
- package/client/variants.js +117 -0
- package/examples/demo-site/hero.svg +18 -0
- package/examples/demo-site/index.html +96 -0
- package/examples/demo-site/styles.css +64 -0
- package/integrations/claude-code/redline.md +27 -0
- package/package.json +42 -0
- package/src/account.js +101 -0
- package/src/bundle.js +33 -0
- package/src/demo.js +137 -0
- package/src/format.js +346 -0
- package/src/guards.js +108 -0
- package/src/inject.js +15 -0
- package/src/mcp.js +380 -0
- package/src/report.js +199 -0
- package/src/server.js +477 -0
- package/src/shots.js +311 -0
- package/src/store.js +544 -0
- package/src/sync.js +126 -0
- package/src/taste.js +80 -0
- package/src/verify.js +71 -0
package/README.md
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# Redline
|
|
2
|
+
|
|
3
|
+
**Review the UI your AI agent built the way you review its code: mark it, send it, check it, accept it.**
|
|
4
|
+
|
|
5
|
+
Describing design problems in chat is slow and lossy ("the second card, no, the one with the yellow…"). With Redline you review the real, running page instead, in turns:
|
|
6
|
+
|
|
7
|
+
- **Mark.** Draw on the page with the pen: cross out what has to go, circle what's off, tick what you love, underline the copy. Or click anything to say why. The shape and its colour are the instruction; words are optional.
|
|
8
|
+
- **Send.** Your marks go to your agent as one round, as a brief with the selector, visible text, size and styles of every element, plus screenshots.
|
|
9
|
+
- **Check.** The agent fixes them in the code and reports back once. Each fix waits for you with a before/after.
|
|
10
|
+
- **Accept**, or **Redo** it in your next round. Nothing is done until you've seen it, and what you approved stays approved: Redline re-checks it after every round.
|
|
11
|
+
|
|
12
|
+
Redline has no runtime dependencies and needs Node ≥ 20. It's built to survive real-world pages: it has been stress-tested by injecting it into 19 production sites (GitHub, YouTube, Stripe, BBC, Airbnb, Apple, Wikipedia, Figma, …) and by an agent-vs-agent review loop, and it handles:
|
|
13
|
+
|
|
14
|
+
- Trusted Types (no `innerHTML`) and a strict `style-src`.
|
|
15
|
+
- Consent iframes and focus-trapping modals.
|
|
16
|
+
- Site hotkeys (typing a note never triggers them).
|
|
17
|
+
- Single-page-app route changes.
|
|
18
|
+
- Hundreds of marks at 60fps.
|
|
19
|
+
|
|
20
|
+
In proxy mode, it patches the site's CSP just enough to allow its own script. The overlay lives in a Shadow DOM, so the site's CSS can't break it and it can't break the site.
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
The board above is [`docs/redline.excalidraw`](docs/redline.excalidraw) (open it at [excalidraw.com](https://excalidraw.com) to edit): who it's for and why, the review loop, and what a feedback item is made of. Regenerate with `node docs/diagrams/generate.mjs && node docs/diagrams/render.mjs`.
|
|
25
|
+
|
|
26
|
+
## Get started in 5 minutes
|
|
27
|
+
|
|
28
|
+
You need Node 20 or newer and Chrome or Chromium (for the before / after screenshots). Accounts live on the hosted Redline at <https://redline-nine-eta.vercel.app> (Vercel + Neon). To run your own instead, see [`web/README.md`](web/README.md).
|
|
29
|
+
|
|
30
|
+
1. **Install Redline**
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
npm install -g @myrtilleio/redline # the `redline` command
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
2. **Open your account page:** <https://redline-nine-eta.vercel.app>. (Self-hosted: `docker compose up -d`, then <http://localhost:4700>, and add `--server http://localhost:4700` when you log in.)
|
|
37
|
+
|
|
38
|
+
3. **Create your account.** Sign up and press **Create key**. The page walks you through the rest, with your key and server filled into every command, and ticks each step off as you do it.
|
|
39
|
+
|
|
40
|
+
4. **Log in, then try the demo**
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
redline login rl_your_key
|
|
44
|
+
redline demo --open
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
5. **Connect your agent** to the hosted MCP server (it works from any machine, cloud agents included):
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
claude mcp add --transport http redline https://redline-nine-eta.vercel.app/api/mcp --header "Authorization: Bearer rl_your_key"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or keep everything on your machine with the local one: `redline init && claude mcp add redline -e REDLINE_API_KEY=rl_your_key -- redline mcp`.
|
|
54
|
+
|
|
55
|
+
6. **Review your own site**
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
redline proxy http://localhost:5173 --cloud # --cloud syncs the project for the hosted MCP server
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Without `--cloud`, your site, notes and screenshots never leave your project; the account only unlocks the tool. `redline whoami` shows which account this machine uses, and `redline logout` forgets the key. A key checked in the last week keeps working offline. Revoke a key on the account page and it stops working everywhere.
|
|
62
|
+
|
|
63
|
+
## Quick start
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
npm i -D /path/to/redline # install from this repo (or run `npm link` in it)
|
|
67
|
+
|
|
68
|
+
redline serve ./site # a static site the agent generated
|
|
69
|
+
redline proxy localhost:5173 # or put it in front of any dev server (Vite, Next, Astro…)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Open the printed URL (default `http://127.0.0.1:4545/`). A toolbar appears at the bottom of the page.
|
|
73
|
+
|
|
74
|
+
To see the whole loop in a minute, try the demo:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
npm run demo # or: redline demo --open
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
It opens a deliberately imperfect landing page where your agent's first round is already back. Four marks were sent: three fixes, and one quote marked *keep*. The scripted agent fixed the three in the files, and, like agents do, quietly restyled the quote while tidying up. A short card says so; press **Review 4 fixes** and accept or redo each fix with **A** or **R**, and watch the guard catch the quote. Then make your own marks and press **Send**. The demo runs on a throwaway copy of `examples/demo-site`.
|
|
81
|
+
|
|
82
|
+
## Mark, Send, Check
|
|
83
|
+
|
|
84
|
+
The toolbar only ever shows the next step: **Pointer**, **Pen**, and whichever turn applies (**Send N**, *With agent*, **Review N**).
|
|
85
|
+
|
|
86
|
+
- **Pen (P):** draw. The shape carries the meaning: ✗ remove, ◯ look here, underline these words, ✓ keep, → move this there. No typing needed. **Click** anything with the pen to box it and say *why*.
|
|
87
|
+
- **Ink:** red means change, green means keep, amber means idea. The gesture usually sets it (a ✓ is green, an ✗ is red); the dot in the receipt after a stroke, or `1` `2` `3`, change it.
|
|
88
|
+
- **Pointer (V):** use the site normally. Click a line or a box's edge to open its note.
|
|
89
|
+
|
|
90
|
+
Everything else sits behind **⋯**: Notes, Box an area (for a region that isn't one element; ↑ walks up to the parent), Devices, Copy for AI, Show original, Hide marks. Each note's extras (Adjust, Reference, the agent's options, Move, Delete) sit behind its own ⋯.
|
|
91
|
+
|
|
92
|
+
**Preview the page:** press **\\** (or ⋯ → Preview the page) to see the site exactly as visitors will, with nothing of Redline on it. Press **\\** again, or the small tab that appears when you move to the bottom-right corner, to bring everything back. It survives the agent's live reloads. To share a clean page, add `?redline=off` to the link: it opens without Redline, just for that visit.
|
|
93
|
+
|
|
94
|
+
**The loop, in turns.** Your marks are drafts until you press **Send** in the toolbar. That hands them to your agent as one round (without a server, Send copies them for your AI chat). The agent fixes what it can, asks *under your mark* when something is unclear, and closes the round with a short report: what it changed, how it reads the page (you can correct it), and at most three questions. You get one signal, "Round 2 is back". The page only ever shows your own marks.
|
|
95
|
+
|
|
96
|
+
## Check
|
|
97
|
+
|
|
98
|
+
Press **Review N** and the page dims; a thin outline marks the first fix, and one card docked above the toolbar says what the agent did and what you asked. The live page *is* the after. The card's **Before / After** switch lays the before over it, exactly where it was: **tap Space** to switch (and tap again to come back), or **hold Space** to peek. The outline takes the old element's size while it's shown, so a fix that resized something visibly grows and shrinks as you flip. The before is taken without your mark on it. When it can't line up with the page (it was taken in the other theme, or the element changed shape), it shows as a framed picture instead. If the agent rebuilt the page so much that the element can't be found (not by its selector, not by its words), the card says so and shows the before and after pictures, and you judge from those. Far items are reached by a quick hop, and the outline stays locked to the element while you scroll. Each fix asks for one key:
|
|
99
|
+
|
|
100
|
+
- **A** accepts it. A green seal lands on it, and the spotlight moves on.
|
|
101
|
+
- **R** redoes it: say what's still wrong (optional) and it becomes a draft for your next round.
|
|
102
|
+
- **← →** move between items, **Esc** stops (the rest wait for later).
|
|
103
|
+
|
|
104
|
+
At the end you get the round's **first-time accept rate**, filling a ring, and one **Send** for whatever you redid.
|
|
105
|
+
|
|
106
|
+
## Guards: what you approve stays approved
|
|
107
|
+
|
|
108
|
+
A green mark and an accepted fix are *approvals*. Redline photographs each one and takes a fingerprint of it as approved: its size, its text, and the styles that make its look, for it and everything inside it. Every time your agent closes a round, each approval is photographed again and compared. If nothing changed, you hear nothing. If something did, the mark comes back to the page with a red **Changed** tab, joins **Review**, and says what changed in words:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
font-size 32px → 24px on blockquote
|
|
112
|
+
font-style italic → normal on blockquote
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**R** sends it back: the agent's next brief says exactly what to restore, next to the screenshot as approved. **A** keeps the new look, which becomes the approved one. An approval that has left the page can be let go.
|
|
116
|
+
|
|
117
|
+
**At every width that matters.** An approval made on a laptop or tablet is also guarded on a phone (390px), unless it isn't shown there. A regression that only breaks the mobile layout comes back as `at 390px: grid-template-columns 342px → 169px 180px on section.stats`.
|
|
118
|
+
|
|
119
|
+
**No crying wolf.** A guard that raises false alarms gets ignored, so they're designed out:
|
|
120
|
+
|
|
121
|
+
- Photographs don't depend on the moment: CSS animations and transitions jump to their end, endless ones stop, videos pause, and images (lazy ones too) are loaded first.
|
|
122
|
+
- Each approval is fingerprinted three times over a second. Whatever changes on its own (a clock, a feed ticking "3s ago", a carousel, numbers counting up) is marked volatile and never compared.
|
|
123
|
+
- What changes between page loads but not within one (a "last updated" stamp) gets one more key in Check: **L**, *changes by itself*. From then on Redline watches its look, not its words and images.
|
|
124
|
+
|
|
125
|
+
**Checked before you see it.** The agent can run the same check before it closes a round, and fix what it broke before you ever see a red tab: `check_approvals` over MCP, or `redline verify <site>`. It records nothing; only the check Redline runs when the round closes reaches you.
|
|
126
|
+
|
|
127
|
+
**In CI.** The same check runs headless, and exits 1 if an approval changed or is gone:
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
redline verify ./site # a static site
|
|
131
|
+
redline verify --url http://localhost:5173 # or a running dev server
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
It needs `.redline/feedback.json` (commit it; baselines are taken while the review is open) and Chrome.
|
|
135
|
+
|
|
136
|
+
**How well it works.** `npm run field-test` runs three rounds on a real Vite + React app that moves by itself (a live feed, count-up numbers, a carousel, entrance animations, a lazy image), with seven approvals at two widths. Scripted, so every alarm can be labelled. Last run: 0 false alarms, the agent's pre-flight caught all three approvals a shared-button edit broke, and the round's guards caught both approvals a phone-only regression broke, with nothing missed. Details in [`examples/field-test/RESULTS.md`](examples/field-test/RESULTS.md). What it can't tell you is the accept rate: that takes a person.
|
|
137
|
+
|
|
138
|
+
## Share the proof
|
|
139
|
+
|
|
140
|
+
A round is worth showing to people who weren't there: a PM, a client, the PR reviewer.
|
|
141
|
+
|
|
142
|
+
- **Round report** (⋯ → *Round report*, or `redline report`): one self-contained HTML page, images inside. It shows the agent's summary and the first-time accept rate, then every fix with what was asked, what was done and a before/after slider (side by side when the fix resized the element), your verdicts and what the guards caught. Send the file to anyone. `--md` prints the same thing as Markdown for a PR comment (`redline report --md | gh pr comment <n> --body-file -`), and `--round <n>` picks an earlier round.
|
|
143
|
+
- **Check link** (⋯ → *Copy review link*, also printed by `serve`/`proxy`): the page with `?redline=check` and a review key. Whoever opens it lands straight in Check, with no drawing tools, and accepts or redoes the fixes like you would; the key lets them do that and nothing else. Their redos go to your agent in the next round. Start Redline with `--host 0.0.0.0` (or behind a tunnel) so the link opens from another machine.
|
|
144
|
+
|
|
145
|
+
### Who can reach your review
|
|
146
|
+
|
|
147
|
+
On your machine, from the page Redline serves, you need nothing. Everyone else needs a key, and the keys change every time Redline starts:
|
|
148
|
+
|
|
149
|
+
- The **review key** rides in the Check link: read the review, accept, redo, send back. It can't add, edit or delete marks, or upload anything.
|
|
150
|
+
- The **owner key** is printed when you bind to another interface (`--host 0.0.0.0`), for opening your own review from another machine.
|
|
151
|
+
- Another website open in your browser can't read or write your review, and neither can a hostname that merely resolves to your machine.
|
|
152
|
+
|
|
153
|
+
Keys leave the address bar as soon as the page opens, so they don't end up in screenshots or bookmarks.
|
|
154
|
+
|
|
155
|
+
## Taste: the agent learns the reviewer
|
|
156
|
+
|
|
157
|
+
Every judgment is kept in `.redline/taste.json` and rendered as **`.redline/TASTE.md`**: what to keep exactly as it is, the fixes you accepted (the direction you like), the ones you rejected and why (never try them again), approvals that broke, and what the page is for in your words. It survives `redline clear`, and the top of every brief carries the latest rejections, so the same mistake doesn't come back two rounds later. The accept rate per round is in there too: it's how you see the agent getting better at your taste.
|
|
158
|
+
|
|
159
|
+
## The designer toolkit
|
|
160
|
+
|
|
161
|
+
Every note is a card that can carry five kinds of evidence, all of which end up in one brief for the agent:
|
|
162
|
+
|
|
163
|
+
| | What you do | What the agent gets |
|
|
164
|
+
|---|---|---|
|
|
165
|
+
| **Adjust** (live edits) | In a note, open **Adjust** and scrub font size, weight, line height, spacing, radius and colours right on the page (drag a label, or ↑/↓ with Shift for ×10). Use **Edit text** to retype wording in place, and tick **Apply to all** to change every similar element. | Exact values: ``font-size` 76px → **48px** on every `h2.section-title``. Your edit stays previewed on the page until the agent ships it. Press `E` to flip between your version and the site as built. |
|
|
166
|
+
| **Screenshots** | Nothing. Redline photographs each item with headless Chrome: as you saw it (with your mark), with your proposed edit, each option, and the result after the fix. | PNG files in `.redline/shots/`, which MCP clients receive as real images. You get a **Compare** slider (before/after) on resolved items. |
|
|
167
|
+
| **Options** | Nothing. When a mark could be fixed several ways, the agent proposes a few instead of guessing. Flip between them live on the page, then **Pick** one. | Your pick, with its exact CSS/HTML to build. Agents propose options with `redline variants <id> --file options.json` or the `propose_variants` MCP tool. |
|
|
168
|
+
| **Devices** (`D`) | See desktop, tablet and mobile side by side, as real iframes at 1440/820/390px, so every breakpoint applies. Select or draw in any of them: notes record that width. One toolbar drives all three, and scrolling syncs. | Notes tagged with the right screen width, and screenshots taken at that width. |
|
|
169
|
+
| **References** | Paste (Ctrl+V), drop, or attach an image into a note: a site you like, a sketch. | Image files in `.redline/assets/`, with the note that says what to take from them. |
|
|
170
|
+
|
|
171
|
+
Writing options: `&` in `css` is replaced with the item's selector everywhere it appears, including inside strings, and previews mark every declaration `!important` so they beat the site's CSS. An `html` option swaps the element's markup for real; on React/Vue pages, reload after previewing one so the framework owns that markup again.
|
|
172
|
+
|
|
173
|
+
Screenshots need Chrome or Chromium installed (set `CHROME_PATH` if it isn't found) and Node 22+. Everything else works without them.
|
|
174
|
+
|
|
175
|
+
## In the browser
|
|
176
|
+
|
|
177
|
+
| Key | |
|
|
178
|
+
|---|---|
|
|
179
|
+
| `P` | **Pen**: draw on the page, or click an element to box it and say why. A second stroke drawn over the first right after joins it (so ✗ is one mark) |
|
|
180
|
+
| `B` | **Box an area**: drag around a region, or click an element. `↑`/`↓` walks to the parent/child, `Enter` boxes it |
|
|
181
|
+
| `V` / `Esc` | **Pointer**: use the site normally, with your marks still visible |
|
|
182
|
+
| `1` `2` `3` | Ink: red (change) / green (keep) / amber (idea), for marks whose shape doesn't say it |
|
|
183
|
+
| `L` | Notes panel, with open and resolved items; click one to jump to it |
|
|
184
|
+
| `C` | Copy all feedback as a prompt for your AI chat |
|
|
185
|
+
| `D` | Devices: desktop, tablet and mobile side by side |
|
|
186
|
+
| `E` | Flip between your proposed edits and the page as built |
|
|
187
|
+
| `H` | Hide or show marks |
|
|
188
|
+
| `Ctrl`/`⌘` + `Z` | Undo the last pen mark |
|
|
189
|
+
| `Ctrl`/`⌘` + `Enter` | Save a note |
|
|
190
|
+
|
|
191
|
+
Every item also records the **screen width** (a note made on a phone is about the mobile layout) and the **page state**: pressed toggles, selected tabs and open menus. An underline records **the words it underlines**. **Move note** (the ↔ button) re-points a note at another element, and deleting can be undone from the toast.
|
|
192
|
+
|
|
193
|
+
Marks are anchored to elements, not screen coordinates. They follow their element through scrolling, resizing and re-layouts, and survive reloads. Your mode (select/pen) is kept across the live reloads triggered by the agent's edits.
|
|
194
|
+
|
|
195
|
+
## For the agent
|
|
196
|
+
|
|
197
|
+
Feedback lives in `.redline/` in the directory you started Redline from:
|
|
198
|
+
|
|
199
|
+
- `.redline/FEEDBACK.md`: the open items as an actionable brief, rewritten on every change
|
|
200
|
+
- `.redline/TASTE.md`: what this reviewer approved and rejected, across every round
|
|
201
|
+
- `.redline/feedback.json`: the full data, including the approvals' fingerprints
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
redline feedback # print the brief (--json, --all, --page /pricing)
|
|
205
|
+
redline wait # block until the reviewer presses Send, then print the brief (--timeout <s>)
|
|
206
|
+
redline resolve 3 --note "Hero title now sentence case at 48px"
|
|
207
|
+
redline reopen 3
|
|
208
|
+
redline clear # drop resolved items (--all: everything); TASTE.md stays
|
|
209
|
+
redline verify ./site # every approval still as approved? exit 1 if not
|
|
210
|
+
redline report --md # the last round, for a PR comment (without --md: an HTML page)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Use single quotes around notes that contain `$` or backticks (`--note 'Now $29/mo'`). Items can be referenced by their pin number (`3`, `#3`) or id (`a_1b2c3d4e`).
|
|
214
|
+
|
|
215
|
+
**Claude Code:** `redline init` adds a `/redline` slash command. It pulls the open feedback into context, applies the fixes, keeps what was marked Good, and resolves each item with a note. Then it runs `redline wait` in the background, so your next Send wakes the agent without you typing anything.
|
|
216
|
+
|
|
217
|
+
**MCP (any MCP-capable agent):** `redline mcp` runs Redline as a stdio MCP server with ten tools:
|
|
218
|
+
|
|
219
|
+
| Tool | What it does |
|
|
220
|
+
|---|---|
|
|
221
|
+
| `start_review` | Serve a folder (`dir`) or proxy a dev server (`url`), and return the URL for the reviewer |
|
|
222
|
+
| `wait_for_round` | Wait until the reviewer presses Send, then return the round's brief (at once if one is waiting). Returns "still waiting" after `timeout_seconds` (default 240) so the agent calls it again; sends progress while it waits and stops when the client cancels |
|
|
223
|
+
| `feedback` | The open items as a brief (or `format: "json"`), optionally for one `page`, plus their screenshots and reference images as images |
|
|
224
|
+
| `propose_variants` | Answer "ask for options": 2–5 alternatives `{label, css, html?, note?}` the reviewer previews and picks from |
|
|
225
|
+
| `reply` | Answer, or ask, under one of the reviewer's marks |
|
|
226
|
+
| `finish_round` | Close the round: a summary, how you read the page, at most 3 questions |
|
|
227
|
+
| `check_approvals` | Before `finish_round`: is everything the reviewer approved still as approved, at every width? Records nothing; returns what changed, in words, with the approved screenshots |
|
|
228
|
+
| `resolve` | Mark items done with a one-line `note`; the reviewer sees it live |
|
|
229
|
+
| `reopen` | Mark items open again |
|
|
230
|
+
| `stop_review` | Stop the session |
|
|
231
|
+
|
|
232
|
+
Register it with Claude Code from your project folder (feedback lives in that folder's `.redline/`):
|
|
233
|
+
|
|
234
|
+
```sh
|
|
235
|
+
claude mcp add redline -- node /path/to/redline/bin/redline.js mcp
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The loop is `wait_for_round` → fix and `resolve` → `finish_round` → `wait_for_round`, so the agent keeps working round after round while you review.
|
|
239
|
+
|
|
240
|
+
Other clients take the same command: `{"command": "node", "args": ["/path/to/redline/bin/redline.js", "mcp"]}`.
|
|
241
|
+
|
|
242
|
+
**Other agents:** tell them *"Read `.redline/FEEDBACK.md`, apply the feedback, and run `redline resolve <id> --note …` for each item."* Or press **Copy for AI** in the toolbar and paste the brief into any chat.
|
|
243
|
+
|
|
244
|
+
An excerpt of the brief:
|
|
245
|
+
|
|
246
|
+
```md
|
|
247
|
+
### 1. ✗ Fix · note · id `a_4f0c9e21`
|
|
248
|
+
- **Target:** `h1.hero-title` — `<h1 class="hero-title">` “THE ULTIMATE REVOLUTIONARY COFFEE TRACKING…”
|
|
249
|
+
- **Comment:** All caps feels shouty. Sentence case, ~48px.
|
|
250
|
+
- **Element now:** 932×380 at (174, 196) · font 76px/76px Georgia 700 · color #7c2d12
|
|
251
|
+
|
|
252
|
+
### 2. ✓ Good · pen mark: circled · id `a_9b1d77aa`
|
|
253
|
+
- **Target:** `div.plan.plan-featured` — `<div class="plan plan-featured">` “Pro $6/mo Unlimited brews…”
|
|
254
|
+
- **Comment:** Love this highlight treatment
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Without a server
|
|
258
|
+
|
|
259
|
+
`redline bundle -o redline.js` writes a standalone script. Add `<script src="redline.js"></script>` to any page (or inject it from devtools). Feedback is then kept in that browser's `localStorage`, and you hand it over with **Copy for AI** or the **.md** download.
|
|
260
|
+
|
|
261
|
+
## How it works
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
browser ── overlay (client/overlay.js, Shadow DOM) ──┐ REST + Server-Sent Events
|
|
265
|
+
▼
|
|
266
|
+
redline serve|proxy (src/server.js) ── injects <script> into every HTML response
|
|
267
|
+
│ watches site files → live reload
|
|
268
|
+
▼
|
|
269
|
+
.redline/feedback.json + FEEDBACK.md ◀── redline feedback / resolve (agent)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
- `src/format.js` turns annotations into the Markdown brief and classifies pen gestures. It is shared by the CLI and the browser, and the bundler inlines it into the client.
|
|
273
|
+
- `src/store.js` is the file-backed store: validated input, serialized writes, atomic renames.
|
|
274
|
+
- `src/server.js` covers CSP patching for the injected script, static serving (with directory listings and traversal protection), the reverse proxy (HTML injection, redirect rewriting, WebSocket pass-through for HMR), the annotation API and SSE.
|
|
275
|
+
|
|
276
|
+
## Development
|
|
277
|
+
|
|
278
|
+
```sh
|
|
279
|
+
npm test # unit + API tests; browser e2e runs when playwright-core and Chromium are present
|
|
280
|
+
REDLINE_SHOTS=/tmp/shots npm test # also save e2e screenshots
|
|
281
|
+
```
|
package/bin/redline.js
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { copyFile, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { buildClient } from '../src/bundle.js';
|
|
7
|
+
import { formatMarkdown } from '../src/format.js';
|
|
8
|
+
import { runMcpServer } from '../src/mcp.js';
|
|
9
|
+
import { startDemo } from '../src/demo.js';
|
|
10
|
+
import { isGuarded } from '../src/guards.js';
|
|
11
|
+
import { reportHtml, reportMarkdown, reportModel } from '../src/report.js';
|
|
12
|
+
import { createRedlineServer } from '../src/server.js';
|
|
13
|
+
import { describeVerify, openVerifier, verifyApprovals } from '../src/verify.js';
|
|
14
|
+
import { Store } from '../src/store.js';
|
|
15
|
+
import { credentialsPath, currentAccount, forget, login, requireAccount, verifyKey } from '../src/account.js';
|
|
16
|
+
import { startSync } from '../src/sync.js';
|
|
17
|
+
import { basename } from 'node:path';
|
|
18
|
+
|
|
19
|
+
const HELP = `redline — review the UI your AI agent built: mark it, send it, check each fix, accept it.
|
|
20
|
+
|
|
21
|
+
Usage
|
|
22
|
+
redline login <key> Save your API key (make one at your Redline account page; --server <url>)
|
|
23
|
+
redline whoami Which account this machine uses
|
|
24
|
+
redline logout Forget the saved key
|
|
25
|
+
redline serve [dir] Serve a static site (default: .) with the overlay + live reload
|
|
26
|
+
redline proxy <url> Put the overlay in front of a running dev server (e.g. http://localhost:5173)
|
|
27
|
+
redline demo Try it: a demo site where your agent's first round is back, ready to check
|
|
28
|
+
redline verify [dir] Check every approval still looks as approved (CI); exits 1 if one changed
|
|
29
|
+
(--url <dev server> instead of a folder)
|
|
30
|
+
redline report [-o file] The last round as one page to share: before/after, verdicts, guards
|
|
31
|
+
(--round <n>; --md for a PR comment, printed)
|
|
32
|
+
redline feedback Print open feedback as a Markdown brief for your agent
|
|
33
|
+
redline wait Wait until the reviewer presses Send, then print the brief (exits at once if
|
|
34
|
+
a round is already waiting; --timeout <s> exits 2 when time runs out)
|
|
35
|
+
redline resolve <id...> Mark items done (--note "what changed"); the reviewer sees it live
|
|
36
|
+
redline reopen <id...> Mark items open again
|
|
37
|
+
redline reply <id> "text" Answer the reviewer in an item's conversation
|
|
38
|
+
redline done --summary "…" --reading "…" [--question "…"]
|
|
39
|
+
Close the round: what you did, how you read the page, up to 3 questions
|
|
40
|
+
redline send Send the reviewer's drafts as the next round (the overlay has a Send button)
|
|
41
|
+
redline variants <id> --file options.json
|
|
42
|
+
Propose options the reviewer asked for: [{label, css, html?, note?}], & = the element
|
|
43
|
+
redline clear Delete resolved items (--all deletes everything)
|
|
44
|
+
redline bundle [-o file] Write the standalone overlay script (no server; saves in localStorage)
|
|
45
|
+
redline init Add the /redline command for Claude Code to this project
|
|
46
|
+
redline mcp Run as an MCP server over stdio (tools include wait_for_round, feedback, resolve, finish_round)
|
|
47
|
+
|
|
48
|
+
Options
|
|
49
|
+
-p, --port <n> Port for serve/proxy (default 4545)
|
|
50
|
+
--host <h> Interface to bind (default 127.0.0.1)
|
|
51
|
+
--open Open the browser after starting
|
|
52
|
+
--store <dir> Where feedback lives (default ./.redline)
|
|
53
|
+
--cloud With serve/proxy: sync this project to your account, for the hosted MCP server
|
|
54
|
+
(--project <name>; default: this folder's name)
|
|
55
|
+
--server <url> Redline accounts server for login (default: the hosted Redline, or REDLINE_SERVER)
|
|
56
|
+
--json Machine-readable output for feedback
|
|
57
|
+
--all Include resolved items (feedback) / everything (clear)
|
|
58
|
+
--page <path> Only feedback for one page, e.g. /pricing
|
|
59
|
+
-n, --note <text> Resolution note for resolve
|
|
60
|
+
--target <css> With resolve: what the "after" screenshot should show, if the fix restructured the element
|
|
61
|
+
-o, --out <file> Output file for bundle
|
|
62
|
+
-h, --help Show this help
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
function fail(msg) {
|
|
66
|
+
console.error(`redline: ${msg}`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// A session (serve, proxy, demo, mcp) starts with a valid account key, or with the
|
|
71
|
+
// steps to get one. Offline, a key checked in the last week still works.
|
|
72
|
+
async function gate(event) {
|
|
73
|
+
const acc = await requireAccount(event);
|
|
74
|
+
if (!acc.ok) {
|
|
75
|
+
console.error(`\n${acc.message}\n`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
if (acc.warning) console.error(`\x1b[33m${acc.warning}\x1b[0m`);
|
|
79
|
+
return acc.user || null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function openBrowser(url) {
|
|
83
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
84
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
85
|
+
spawn(cmd, args, { stdio: 'ignore', detached: true }).on('error', () => {}).unref();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function listen(server, port, host) {
|
|
89
|
+
return new Promise((ok, err) => {
|
|
90
|
+
server.once('error', err);
|
|
91
|
+
server.listen(port, host, () => ok(server.address().port));
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function start(kind, opts, arg) {
|
|
96
|
+
const user = await gate('session');
|
|
97
|
+
const storeDir = resolve(opts.store);
|
|
98
|
+
const time = () => new Date().toTimeString().slice(0, 8);
|
|
99
|
+
const log = (msg) => console.log(`\x1b[2m${time()}\x1b[0m ${msg}`);
|
|
100
|
+
let server;
|
|
101
|
+
if (kind === 'serve') {
|
|
102
|
+
const root = resolve(arg || '.');
|
|
103
|
+
const info = await stat(root).catch(() => null);
|
|
104
|
+
if (!info?.isDirectory()) fail(`not a directory: ${root}`);
|
|
105
|
+
server = createRedlineServer({ root, storeDir, log });
|
|
106
|
+
} else {
|
|
107
|
+
if (!arg) fail('proxy needs a target URL, e.g. redline proxy http://localhost:5173');
|
|
108
|
+
let target;
|
|
109
|
+
try {
|
|
110
|
+
target = new URL(/^https?:\/\//.test(arg) ? arg : `http://${arg}`);
|
|
111
|
+
} catch {
|
|
112
|
+
fail(`invalid URL: ${arg}`);
|
|
113
|
+
}
|
|
114
|
+
server = createRedlineServer({ target: target.origin, storeDir, log });
|
|
115
|
+
}
|
|
116
|
+
const port = await listen(server, Number(opts.port), opts.host).catch((e) =>
|
|
117
|
+
fail(e.code === 'EADDRINUSE' ? `port ${opts.port} is in use — try --port ${Number(opts.port) + 1}` : e.message),
|
|
118
|
+
);
|
|
119
|
+
const url = `http://${opts.host === '0.0.0.0' ? 'localhost' : opts.host}:${port}/`;
|
|
120
|
+
console.log(`\n \x1b[1;31mRedline\x1b[0m ${kind === 'serve' ? `serving ${resolve(arg || '.')}` : `proxying ${arg}`}`);
|
|
121
|
+
console.log(` \x1b[1m${url}\x1b[0m`);
|
|
122
|
+
console.log(` feedback → ${storeDir}/FEEDBACK.md`);
|
|
123
|
+
if (user) console.log(` \x1b[2msigned in as ${user.email}\x1b[0m`);
|
|
124
|
+
const local = /^(127\.|localhost$|::1$)/.test(opts.host);
|
|
125
|
+
// On this machine you need no key. Anyone else does: the owner link gives everything,
|
|
126
|
+
// the Check link only verdicts. Both change every time Redline starts.
|
|
127
|
+
if (!local) console.log(` \x1b[2myou, from another machine →\x1b[0m ${url}?rk=${server.keys.owner}`);
|
|
128
|
+
console.log(` \x1b[2mshare Check (verdicts only) →\x1b[0m ${url}?redline=check&rk=${server.keys.review}${local ? ' \x1b[2m(add --host 0.0.0.0 to open it from another machine)\x1b[0m' : ''}\n`);
|
|
129
|
+
console.log(' In the page: P draw on anything, or click it to say why · Send to your agent · Check what comes back\n');
|
|
130
|
+
if (opts.open) openBrowser(url);
|
|
131
|
+
// --cloud: mirror this project to your account, for the hosted MCP server.
|
|
132
|
+
let sync = null;
|
|
133
|
+
if (opts.cloud) {
|
|
134
|
+
const { key, server: account } = await currentAccount();
|
|
135
|
+
if (!key) fail('--cloud needs an account: redline login <your-key>');
|
|
136
|
+
const project = (opts.project || basename(process.cwd())).slice(0, 120);
|
|
137
|
+
sync = startSync({ store: server.store, storeDir, server: account, key, project, log });
|
|
138
|
+
console.log(` \x1b[1mcloud\x1b[0m syncing as project “${project}” · hosted MCP: ${account}/api/mcp\n`);
|
|
139
|
+
}
|
|
140
|
+
const stop = () => (sync && sync.stop(), server.close(() => process.exit(0)));
|
|
141
|
+
process.on('SIGINT', stop);
|
|
142
|
+
process.on('SIGTERM', stop);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function demo(opts) {
|
|
146
|
+
await gate('demo');
|
|
147
|
+
const time = () => new Date().toTimeString().slice(0, 8);
|
|
148
|
+
const log = (msg) => console.log(`\x1b[2m${time()}\x1b[0m ${msg}`);
|
|
149
|
+
const { server, ready } = await startDemo({ port: Number(opts.port), host: opts.host, log }).catch((e) =>
|
|
150
|
+
fail(e.code === 'EADDRINUSE' ? `port ${opts.port} is in use — try --port ${Number(opts.port) + 1}` : e.message),
|
|
151
|
+
);
|
|
152
|
+
const stop = () => server.close(() => process.exit(0));
|
|
153
|
+
process.on('SIGINT', stop);
|
|
154
|
+
process.on('SIGTERM', stop);
|
|
155
|
+
const url = `http://${opts.host === '0.0.0.0' ? 'localhost' : opts.host}:${server.address().port}/`;
|
|
156
|
+
console.log(`\n \x1b[1;31mRedline\x1b[0m demo: a first draft, and your agent's first round of fixes`);
|
|
157
|
+
if (server.shots.available) console.log(' Taking the before screenshots…');
|
|
158
|
+
await ready;
|
|
159
|
+
console.log(`\n \x1b[1m${url}\x1b[0m`);
|
|
160
|
+
console.log(' Round 1 is back. Press \x1b[1mReview\x1b[0m: accept or redo each fix, and see what the guards caught.');
|
|
161
|
+
console.log(' Then draw on anything with P, or click it to say why, and press Send.\n');
|
|
162
|
+
if (opts.open) openBrowser(url);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Guards, headless: photograph every approval again and compare it with how it was
|
|
166
|
+
// approved, at every width it's guarded at. Records nothing. For the agent before it
|
|
167
|
+
// closes a round, and for CI: exits 1 if any approval changed or is gone.
|
|
168
|
+
async function verify(opts, dir) {
|
|
169
|
+
const storeDir = resolve(opts.store);
|
|
170
|
+
let where;
|
|
171
|
+
if (opts.url) {
|
|
172
|
+
try {
|
|
173
|
+
where = { target: new URL(/^https?:\/\//.test(opts.url) ? opts.url : `http://${opts.url}`).origin };
|
|
174
|
+
} catch {
|
|
175
|
+
fail(`invalid URL: ${opts.url}`);
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
const root = resolve(dir || '.');
|
|
179
|
+
if (!(await stat(root).catch(() => null))?.isDirectory()) fail(`not a directory: ${root}`);
|
|
180
|
+
where = { root };
|
|
181
|
+
}
|
|
182
|
+
const v = await openVerifier({ ...where, storeDir });
|
|
183
|
+
const done = async (code) => {
|
|
184
|
+
await v.close();
|
|
185
|
+
process.exit(code);
|
|
186
|
+
};
|
|
187
|
+
if (!v.shots.available) {
|
|
188
|
+
console.error('redline: verify needs Chrome or Chromium (set CHROME_PATH) and Node 22+');
|
|
189
|
+
return done(2);
|
|
190
|
+
}
|
|
191
|
+
const count = (await v.store.all()).filter(isGuarded).length;
|
|
192
|
+
if (!count) {
|
|
193
|
+
console.log('No approvals to verify yet: mark something green, or accept a fix, first.');
|
|
194
|
+
return done(0);
|
|
195
|
+
}
|
|
196
|
+
console.log(`\n \x1b[1;31mRedline\x1b[0m verifying ${count} approval${count === 1 ? '' : 's'}\n`);
|
|
197
|
+
const { lines, summary, bad } = describeVerify(await verifyApprovals(v), { storeDir: opts.store, color: true });
|
|
198
|
+
console.log(lines.join('\n'));
|
|
199
|
+
console.log(`\n ${summary}\n`);
|
|
200
|
+
return done(bad ? 1 : 0);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function main() {
|
|
204
|
+
const { values: opts, positionals } = parseArgs({
|
|
205
|
+
allowPositionals: true,
|
|
206
|
+
options: {
|
|
207
|
+
port: { type: 'string', short: 'p', default: '4545' },
|
|
208
|
+
host: { type: 'string', default: '127.0.0.1' },
|
|
209
|
+
open: { type: 'boolean', default: false },
|
|
210
|
+
store: { type: 'string', default: '.redline' },
|
|
211
|
+
json: { type: 'boolean', default: false },
|
|
212
|
+
all: { type: 'boolean', default: false },
|
|
213
|
+
page: { type: 'string' },
|
|
214
|
+
note: { type: 'string', short: 'n', default: '' },
|
|
215
|
+
out: { type: 'string', short: 'o' },
|
|
216
|
+
file: { type: 'string', short: 'f' },
|
|
217
|
+
target: { type: 'string' },
|
|
218
|
+
url: { type: 'string' },
|
|
219
|
+
server: { type: 'string' },
|
|
220
|
+
cloud: { type: 'boolean', default: false },
|
|
221
|
+
project: { type: 'string' },
|
|
222
|
+
round: { type: 'string' },
|
|
223
|
+
timeout: { type: 'string' },
|
|
224
|
+
md: { type: 'boolean', default: false },
|
|
225
|
+
summary: { type: 'string' },
|
|
226
|
+
reading: { type: 'string' },
|
|
227
|
+
question: { type: 'string', multiple: true },
|
|
228
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
const [cmd, ...args] = positionals;
|
|
232
|
+
if (opts.help || !cmd) return console.log(HELP);
|
|
233
|
+
const store = new Store(resolve(opts.store));
|
|
234
|
+
|
|
235
|
+
switch (cmd) {
|
|
236
|
+
case 'serve':
|
|
237
|
+
case 'proxy':
|
|
238
|
+
return start(cmd, opts, args[0]);
|
|
239
|
+
|
|
240
|
+
case 'demo':
|
|
241
|
+
return demo(opts);
|
|
242
|
+
|
|
243
|
+
case 'verify':
|
|
244
|
+
return verify(opts, args[0]);
|
|
245
|
+
|
|
246
|
+
case 'report': {
|
|
247
|
+
const model = reportModel(await store.all(), await store.rounds(), opts.round);
|
|
248
|
+
if (!model) fail('no round to report yet: send one first');
|
|
249
|
+
if (opts.md) return process.stdout.write(reportMarkdown(model, { storeDir: opts.store }));
|
|
250
|
+
const out = resolve(opts.out || `${opts.store}/report-round-${model.round.n}.html`);
|
|
251
|
+
await writeFile(out, await reportHtml(model, { storeDir: resolve(opts.store) }));
|
|
252
|
+
return console.log(`wrote ${out} (one file, images inside: send it to anyone)`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
case 'feedback': {
|
|
256
|
+
let list = await store.list(opts.page);
|
|
257
|
+
if (opts.json) {
|
|
258
|
+
if (!opts.all) list = list.filter((a) => a.status !== 'resolved');
|
|
259
|
+
return console.log(JSON.stringify(list, null, 2));
|
|
260
|
+
}
|
|
261
|
+
return process.stdout.write(formatMarkdown(list, { cli: true, includeResolved: opts.all, rounds: (await store.rounds()).rounds, taste: await store.taste() }));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Blocks until the reviewer presses Send (or returns at once if a round is already
|
|
265
|
+
// waiting), then prints the brief. Run it in the background and the agent is woken
|
|
266
|
+
// when it exits: wait → fix → done → wait.
|
|
267
|
+
case 'wait': {
|
|
268
|
+
const secs = Number(opts.timeout || 0);
|
|
269
|
+
if (!Number.isFinite(secs) || secs < 0) fail(`invalid --timeout: ${opts.timeout}`);
|
|
270
|
+
const stop = new AbortController();
|
|
271
|
+
process.on('SIGINT', () => stop.abort());
|
|
272
|
+
process.on('SIGTERM', () => stop.abort());
|
|
273
|
+
if (!(await store.pendingRound())) console.error(`waiting for the reviewer to send a round (${secs ? `up to ${secs}s` : 'no time limit'})…`);
|
|
274
|
+
const round = await store.waitForRound({ timeoutMs: secs * 1000, signal: stop.signal });
|
|
275
|
+
if (!round) {
|
|
276
|
+
console.log(stop.signal.aborted ? 'stopped waiting' : `no round yet after ${secs}s: run \`redline wait\` again to keep waiting`);
|
|
277
|
+
process.exit(2);
|
|
278
|
+
}
|
|
279
|
+
const list = (await store.list()).filter((a) => a.status !== 'resolved' && !a.accepted && !a.draft);
|
|
280
|
+
if (opts.json) return console.log(JSON.stringify({ round, items: list }, null, 2));
|
|
281
|
+
console.log(`Round ${round.n} is here: ${round.count} item${round.count === 1 ? '' : 's'} sent at ${round.sentAt}.\n`);
|
|
282
|
+
return process.stdout.write(formatMarkdown(list, { cli: true, rounds: (await store.rounds()).rounds, taste: await store.taste() }));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
case 'resolve':
|
|
286
|
+
case 'reopen': {
|
|
287
|
+
if (!args.length) fail(`${cmd} needs at least one id (see \`redline feedback\`)`);
|
|
288
|
+
const all = await store.all();
|
|
289
|
+
for (const ref of args) {
|
|
290
|
+
// Accept the id (a_1b2c3d4e) or the number shown on the pin (#3 / 3).
|
|
291
|
+
const a = all.find((x) => x.id === ref) || all.find((x) => String(x.n) === ref.replace(/^#/, ''));
|
|
292
|
+
if (!a) fail(`no feedback item ${ref}`);
|
|
293
|
+
if (cmd === 'resolve') await store.resolve(a.id, opts.note, opts.target);
|
|
294
|
+
else await store.update(a.id, { status: 'open' });
|
|
295
|
+
console.log(`${cmd === 'resolve' ? '✓ resolved' : '↺ reopened'} #${a.n} (${a.id})`);
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
case 'reply': {
|
|
301
|
+
if (!args[0] || !args[1]) fail('usage: redline reply <id> "your answer"');
|
|
302
|
+
const a = await store.agentSays(args[0], args.slice(1).join(' '));
|
|
303
|
+
return console.log(`replied on #${a.n}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
case 'send': {
|
|
307
|
+
const round = await store.send();
|
|
308
|
+
return console.log(round ? `sent round ${round.n} (${round.count} items)` : 'nothing to send');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
case 'done': {
|
|
312
|
+
const round = await store.finishRound({ summary: opts.summary || '', reading: opts.reading || '', questions: opts.question || [] });
|
|
313
|
+
return console.log(`closed round ${round.n}; the reviewer sees your report`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
case 'variants': {
|
|
317
|
+
if (!args[0] || !opts.file) fail('usage: redline variants <id> --file options.json');
|
|
318
|
+
let list;
|
|
319
|
+
try {
|
|
320
|
+
list = JSON.parse(await readFile(opts.file, 'utf8'));
|
|
321
|
+
} catch (err) {
|
|
322
|
+
fail(`could not read ${opts.file}: ${err.message}`);
|
|
323
|
+
}
|
|
324
|
+
const a = await store.proposeVariants(args[0], list);
|
|
325
|
+
return console.log(`proposed ${a.variants.length} options for #${a.n}: ${a.variants.map((v) => `${v.key} ${v.label}`).join(', ')}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
case 'clear': {
|
|
329
|
+
const n = await store.prune((a) => opts.all || a.status === 'resolved');
|
|
330
|
+
return console.log(`removed ${n} item${n === 1 ? '' : 's'}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
case 'bundle': {
|
|
334
|
+
const js = buildClient();
|
|
335
|
+
if (!opts.out) return process.stdout.write(js);
|
|
336
|
+
await writeFile(opts.out, js);
|
|
337
|
+
return console.log(`wrote ${opts.out} — add <script src="${opts.out}"></script> to any page`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
case 'mcp':
|
|
341
|
+
// Stdout is the protocol: an account problem is told through the tools instead.
|
|
342
|
+
return runMcpServer({ storeDir: resolve(opts.store), account: requireAccount('mcp', { client: 'redline mcp' }) });
|
|
343
|
+
|
|
344
|
+
case 'login': {
|
|
345
|
+
const user = await login(args[0], { server: opts.server }).catch((err) => fail(err.message));
|
|
346
|
+
return console.log(`✓ Signed in as ${user.email}. Saved in ${credentialsPath()}\n Next: redline demo --open`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
case 'logout': {
|
|
350
|
+
await forget();
|
|
351
|
+
return console.log('Signed out: the saved key is gone from this machine (revoke it in your account page to stop it everywhere).');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
case 'whoami': {
|
|
355
|
+
const { key, server, fromEnv } = await currentAccount();
|
|
356
|
+
if (!key) fail(`not signed in. Run: redline login <your-key> (make one at ${server})`);
|
|
357
|
+
const v = await verifyKey({ key, server });
|
|
358
|
+
if (!v.ok) fail(v.message);
|
|
359
|
+
return console.log(`${v.user.email} (${v.user.name}) · key ${v.key.start}… on ${server}${fromEnv ? ' · from REDLINE_API_KEY' : ''}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
case 'init': {
|
|
363
|
+
const dest = resolve('.claude/commands/redline.md');
|
|
364
|
+
await mkdir(resolve('.claude/commands'), { recursive: true });
|
|
365
|
+
await copyFile(new URL('../integrations/claude-code/redline.md', import.meta.url), dest);
|
|
366
|
+
console.log(`added ${dest}\nIn Claude Code, run /redline to apply the feedback from the overlay.`);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
default:
|
|
371
|
+
fail(`unknown command "${cmd}"\n\n${HELP}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
main().catch((err) => fail(err.message));
|