retake-demos 0.13.1 → 0.14.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/.env.example CHANGED
@@ -1,9 +1,7 @@
1
- # Your app's demo account, for demos that start behind a login. You rarely edit
2
- # this by hand: the agent asks by name and the Retake window (or `retake secret
3
- # APP_USER APP_PASSWORD`) writes it here. Stays on this machine; never sent to the
4
- # agent. A manifest references ${APP_USER} with `secret: true` — never the values.
5
- # APP_TOTP_SECRET is the authenticator enrolment secret; ${TOTP:APP_TOTP_SECRET}
6
- # becomes the current code. Blank lines below count as unset.
1
+ # Logins are not kept here any more: the Retake window (or `retake login <url>`)
2
+ # saves one per site in your Keychain, and demos sign in with ${LOGIN_USER} and
3
+ # ${LOGIN_PASSWORD}. A value set below still wins over a saved login, and the
4
+ # ${APP_USER}-style names older demos use keep working. Blank lines count as unset.
7
5
  APP_USER=
8
6
  APP_PASSWORD=
9
7
  APP_TOTP_SECRET=
package/AGENTS.md CHANGED
@@ -23,6 +23,7 @@ that wastes real time. Almost nothing needs the camera twice.
23
23
  |---|---|---|
24
24
  | size, shape, format | `retake render <dir> --preset <other>` | seconds |
25
25
  | caption words, speed, zoom, cards, music | edit the manifest, `retake render` | seconds |
26
+ | join separate recorded outputs | `retake stitch outputs/a outputs/b --out outputs/joined/demo.mp4 --transition fade` | render only |
26
27
  | **where** a caption or still sits | `nudge: <ms>` on the scene, then render | seconds |
27
28
  | the ENDING of a demo | `retake run … --from <scene>` | the tail only |
28
29
  | one beat, while iterating | `retake run … --until <scene>` | the head only |
@@ -38,6 +39,13 @@ versions of a demo and `retake restore <dir> <id>` puts one back;
38
39
  `--apply`. Neither ever touches a raw recording or a take.json — those are
39
40
  the only things a re-render cannot rebuild.
40
41
 
42
+ `retake stitch` accepts output folders or video files in playback order. It
43
+ uses the existing transition styles and the first clip's canvas and frame
44
+ rate. Other clips fit without cropping. Source audio stays aligned and
45
+ crossfades with non-cut joins. Use `--no-audio` for silence. The output must
46
+ be a new MP4 path. This command creates no recording or library entry and
47
+ does not synthesize or approve narration. See `docs/stitch.md`.
48
+
41
49
 
42
50
  ## Starting from nothing (the person pasted the repo link)
43
51
 
@@ -57,7 +65,9 @@ it exists.
57
65
  ## The loop an agent should run
58
66
 
59
67
  ```
60
- write demos/<name>.yaml
68
+ retake routine the locked standard — read it BEFORE drafting
69
+ → write demos/<name>.yaml
70
+ → retake routine apply demos/<name>.yaml stamp format, captions, voice, rate
61
71
  → retake validate demos/<name>.yaml schema + warnings (instant)
62
72
  → retake dry demos/<name>.yaml every selector & wait, no camera (~30–60s)
63
73
  → fix what dry reports, repeat until "all N steps resolved"
@@ -81,6 +91,41 @@ seconds; a failed `run` costs two minutes and a confusing video. When `dry`
81
91
  fails it prints the step, the error, and the text that was on screen — read
82
92
  that before changing anything.
83
93
 
94
+ ## First call in any session: `retake routine`
95
+
96
+ It prints the locked standard and, when the product has one, names its **brief** (`demos/capture.md` for Capture). Over MCP, the `routine` tool returns the whole brief. Read it before writing a step: where the app runs, what is on the board, which selectors hold, and the pitfalls that already cost takes.
97
+
98
+ ## The settings are not yours to choose — `retake routine`
99
+
100
+ A workspace has a standard: output format, captions on or off, which voice
101
+ narration uses and how fast it speaks, music or none. It lives in
102
+ `demos/routine.json`, a person set it, and it is applied by the machine.
103
+
104
+ Read it with `retake routine` before you draft, and **do not set these
105
+ yourself**: `preset`, `layout`, `captions`, `viewport`, `scale`,
106
+ `voiceover.voice`, `voiceover.ratePercent`, `music`. Choosing them per demo is
107
+ exactly how one video comes back narrated too fast, the next too slow, and a
108
+ third in a voice nobody asked for.
109
+
110
+ ```
111
+ retake env up bring the app up isolated, reset, and proven — before any take
112
+ retake routine what every recording here will look and sound like
113
+ retake routine learn read the standard off demos already recorded here
114
+ retake routine apply <manifest> stamp the standard onto a manifest you drafted
115
+ retake routine run work demos/plan.json: stamp, dry, record, finish
116
+ retake routine check would a viewer see ONE set? measures the finished files
117
+ retake routine reference <video> the north star every cut is scored against
118
+ ```
119
+
120
+ `retake routine apply` never turns narration ON — that still needs a script and
121
+ a person who has heard it. It only fixes the voice and rate of narration that
122
+ already exists, and strips any `viewport`/`scale` pin, which silently beats the
123
+ preset and is the one way two takes of the same app come out different shapes.
124
+
125
+ Writing the steps, the captions and the story is still entirely your job.
126
+ `demos/style.md` is the taste that needs judgement; the routine is the part
127
+ that only needs to be the same every time.
128
+
84
129
  ## How to tell Claude Code / Codex to use it
85
130
 
86
131
  From the Retake folder:
@@ -186,24 +231,29 @@ Full reference: `README.md` → "Demo-as-code". The short version:
186
231
 
187
232
  Most real demos start behind a sign-in. The agent never sees a password:
188
233
 
189
- - **Ask by name, not for the value.** Over MCP, call `secrets` with the
190
- variable names (`APP_USER`, `APP_PASSWORD`, `APP_TOTP_SECRET` for an
191
- authenticator) and a one-line why. With the Retake window open, a form
192
- appears there and the values go straight into the workspace `.env` — the
193
- tool returns "set". Without a window, the tool returns the sentence to
194
- relay: the person runs `retake secret APP_USER APP_PASSWORD` (typed into
195
- their terminal, hidden, kept on their machine) and the agent calls
196
- `secrets` again. From the CLI only, say the same sentence yourself.
197
- - **Put the sign-in under `auth.setup`** with `${APP_USER}` / `${APP_PASSWORD}`
198
- and `secret: true`, plus `auth.storageState: .auth/<name>.json`. It runs
199
- before the camera, is trimmed off, and the session is reused by later takes.
200
- A failed sign-in never saves a session.
201
- - **Authenticator codes:** `${TOTP:APP_TOTP_SECRET}` is the current six-digit
202
- code, computed when the field is filled.
203
- - **SMS, SSO, captcha:** a script cannot; a person can, once.
204
- `retake signin demos/<name>.yaml` opens a real window, they log in, press
205
- Enter, and the session is saved. Leave `auth.setup` empty; a take with no
206
- fresh session and no setup refuses to run rather than record logged out.
234
+ - **Ask for the site, not for the value.** Over MCP, call `login` with the
235
+ app's URL and a one-line why. A login is saved once per site (in the
236
+ person's Keychain) and reused by every demo of that site, so it often
237
+ answers "Ready" at once, with the exact manifest lines. Otherwise a form
238
+ opens in the Retake window — relay the sentence the tool returns, link
239
+ included, wait for "done", call `login` again. Without a window the
240
+ person runs `retake login <url>` (typed into their terminal, hidden). From
241
+ the CLI only, say the same sentence yourself.
242
+ - **Put the sign-in under `auth.setup`** with `${LOGIN_USER}` /
243
+ `${LOGIN_PASSWORD}` and `secret: true`, plus the per-site
244
+ `auth.storageState` that `login` gives you. It runs before the camera, is
245
+ trimmed off, and the session is reused by later takes; when it runs out,
246
+ Retake signs in again by itself. A failed sign-in never saves a session.
247
+ - **Authenticator codes:** `${TOTP:LOGIN_TOTP}` is the current six-digit
248
+ code, computed when the field is filled — present when the person saved
249
+ an authenticator key.
250
+ - **SMS, SSO, captcha:** a script cannot; a person can, once. The same form
251
+ has "Sign in by hand" (or `retake login <url> --by-hand`): a real window
252
+ opens, they sign in, press Done, and the session is kept. Leave
253
+ `auth.setup` empty; a take with no fresh session and no setup refuses to
254
+ run rather than record logged out.
255
+ - **Other values** (API tokens, keys) still go through `secrets` into the
256
+ workspace `.env` by name, referenced as `${NAME}` with `secret: true`.
207
257
  - **Firebase-style sessions** (IndexedDB) cannot be restored from a saved
208
258
  session: put the login under plain `setup` so it runs every take.
209
259
  - **Demo accounts only.** The output is a video.
@@ -235,9 +285,14 @@ Most real demos start behind a sign-in. The agent never sees a password:
235
285
  control where nothing is wrong, then reports recall and false positives.
236
286
  Expensive: nine takes, about ten minutes, needs the app running. The report
237
287
  it leaves is cheap to read, and **you should read it before telling anybody
238
- how much a sweep finding is worth.** Its stated limits travel with it: the
239
- seeds were written by somebody who has read the checklist, so recall is an
240
- upper bound.
288
+ how much a sweep finding is worth.** Its stated limits travel with it, and
289
+ all four belong in anything you say about the numbers: the seeds were written
290
+ by somebody who has read the checklist, so recall is an upper bound; injected
291
+ faults are cleaner than real ones; the run names the checklist items nothing
292
+ seeds, and says nothing whatever about those; and a run whose control did not
293
+ record cleanly measured nothing at all and exits 3 rather than printing a
294
+ reassuring zero. From MCP you can only READ the last measurement — making one
295
+ is a terminal command, because it records for ten minutes.
241
296
  - `retake intent` — what the product IS, in the builder's words: what it does,
242
297
  who uses it, and what about it is deliberate. Every visual check reads it.
243
298
  Without it they judge the app as a generic web page — measured on a real
package/README.md CHANGED
@@ -42,16 +42,27 @@ mkdir my-demos && cd my-demos
42
42
  retake install
43
43
  ```
44
44
 
45
- `retake install` makes the folder a workspace (`demos/`, `outputs/`, `.env`, the `.gitignore` lines), downloads Chromium, registers the tools with Claude Code and installs its *recording-product-demos* skill, and prints the config for Codex. Restart your agent afterwards — agents load tools at the start of a session.
45
+ `retake install` makes the folder a workspace (`demos/`, `outputs/`, `.env`, and the `.gitignore` lines), downloads Chromium, registers the tools with Claude Code, installs its *recording-product-demos* skill, and prints the config for Codex. Restart your agent afterwards because agents load tools at the start of a session.
46
46
 
47
- Prefer not to install globally? `npx retake-demos install` works the same. Don't use Claude Code? `retake agent` prints the MCP config for Codex, Cursor, or anything else.
47
+ Start `retake ui` and open http://localhost:4310. On an empty workspace, Retake asks only for:
48
48
 
49
- Then, in your agent:
49
+ 1. the public or localhost URL;
50
+ 2. the result the viewer should see;
51
+ 3. the final sound: silent, Edge, local Aiden, or local Kokoro;
52
+ 4. confirmation of the request.
53
+
54
+ The first draft is always silent. Choosing a local voice plays a packaged sample and asks for consent, but it does not download a model or generate speech. Narration starts only after you approve the recording and its continuous script.
55
+
56
+ Prefer not to install globally? `npx retake-demos install` works the same. Don't use Claude Code? `retake agent` prints the MCP config for Codex, Cursor, or another MCP client.
57
+
58
+ If you already have demos, ask your agent directly:
50
59
 
51
60
  > Record a demo of my app at localhost:3000 showing a new user creating their first project.
52
61
 
53
62
  and in another terminal, `retake ui` → http://localhost:4310 to watch it happen. The agent reports its plan and progress there; the video, stills and receipts land there when it's done.
54
63
 
64
+ Not sure what to request? Use [How to prompt an agent to use Retake](docs/prompting-retake.md) to choose the recording type, format, voice, music, camera, branding, privacy rules, and delivery checks.
65
+
55
66
  `retake doctor` tells you if anything is missing.
56
67
 
57
68
  ## What you get
@@ -82,6 +93,10 @@ retake validate demos/x.yaml # schema + warnings, instant
82
93
  retake dry demos/x.yaml # every selector and wait, no camera (~30s)
83
94
  retake run demos/x.yaml --preset draft # first take, quarter-size, fast
84
95
  retake check outputs/x # resolution, fps, duration, files: pass/fail
96
+ retake golden approve finished.mp4 --id short-landscape --title "Short landscape" --approved-by "Your name" --rubric story=pass,framing=pass,timing=pass,text=pass,motion=pass # interactive human approval
97
+ retake golden add finished.mp4 --approval-receipt goldens/.approvals/short-landscape-<hash>.json # store evidence; never copies the MP4
98
+ retake golden verify # integrity plus the required 5–8 visual families
99
+ retake golden score candidate.mp4 --against short-landscape # cheap deterministic media scorecard
85
100
  retake verify outputs/x # did it LOOK right: each scene's `expect`, judged (exit 3 on failure)
86
101
  retake run demos/x.yaml --brisk # the steps without the pacing — for iterating, not for keeping
87
102
  retake intent # what your product IS — context every visual check needs
@@ -100,6 +115,178 @@ The expensive step comes last on purpose. `dry` catches most failures in seconds
100
115
 
101
116
  Exit codes: `0` good · `2` a step failed or the video is a raw fallback · `3` `dry`/`check` failed.
102
117
 
118
+ To join separate recordings, use [`retake stitch`](docs/stitch.md). Supply existing output folders or video files, a new MP4 path, and one transition style.
119
+
120
+ ## The recording environment — what has to be true before the camera
121
+
122
+ Six takes in one session were lost to the environment and not one to the
123
+ recorder: an app pointed at the owner's real cloud store, a browser tab still
124
+ attached that pushed the owner's board back into the "isolated" instance, a dev
125
+ server caching its board so the seed only applied at boot, and two runs racing
126
+ one output folder. Every one is cheap to assert and expensive to read off a
127
+ frame.
128
+
129
+ ```bash
130
+ retake env up # restart it clean, clear its state, then PROVE it
131
+ retake env check # prove the running one without restarting
132
+ retake env down
133
+ ```
134
+
135
+ `demos/environment.yaml` declares it:
136
+
137
+ ```yaml
138
+ name: capture-local
139
+ start:
140
+ command: npm run dev -- --port 3179
141
+ cwd: ~/capture-local
142
+ port: 3179
143
+ env:
144
+ # Blanked, not unset: .env files are read AFTER the process environment,
145
+ # so "" is what stops a real token reaching a real account.
146
+ UPSTASH_REDIS_REST_URL: ""
147
+ APP_PASSWORD: ""
148
+ reset:
149
+ restart: true # the app caches state; without this, take two
150
+ clear: [".retake/sync"] # inherits whatever take one left behind
151
+ expect:
152
+ path: /app
153
+ present: ["button.tab:has-text('Intentions')"]
154
+ absent: ["text=a real note of mine"] # name what must NOT be there
155
+ ```
156
+
157
+ `absent` is the one that earns its place. A present-only check cannot see real
158
+ data leaking in, and you cannot assert the absence of something you did not
159
+ think of — so name it. `retake routine run` picks the file up automatically and
160
+ **refuses to record** if the environment is not what it claims.
161
+
162
+ ## Routines — the settings nobody should decide twice
163
+
164
+ `demos/style.md` is taste an agent reads while drafting, so every draft is a
165
+ fresh interpretation of it. That is how one video comes back narrated too fast,
166
+ the next too slow, and a third in a voice nobody asked for.
167
+
168
+ A routine is the other half: format, captions, voice, speaking rate and music
169
+ held as data and **stamped onto the manifest after it is drafted**. The model
170
+ writes the story; the routine writes the settings; they never argue.
171
+
172
+ **A new product in one step.** Point at the app and at one video you love:
173
+
174
+ ```bash
175
+ retake routine setup http://localhost:3000/app --like "~/Videos/the-one-we-liked.mp4"
176
+ ```
177
+
178
+ Everything that can be read off those two arrives decided: the shape, the
179
+ cards and their colours from the video; the brand's mark from the page; an
180
+ environment that starts your app on its own port with every token in `.env`
181
+ blanked (names only — a value never leaves its file); and a brief with the
182
+ selectors on the first screen, so the next agent starts informed. What cannot
183
+ be read — the closing card's words, the one real thing that must never appear —
184
+ is asked once, at the end. A live site works too; there is just nothing to start.
185
+
186
+ **Start by reading, not by guessing.** The first routine used to be Retake's
187
+ defaults — an opinion wearing the clothes of a standard, which `apply` then
188
+ stamps over everything with confidence. Read it off demos you already shipped:
189
+
190
+ ```bash
191
+ retake routine learn # what your existing manifests actually agree on
192
+ retake routine learn --write # make that the standard
193
+ ```
194
+
195
+ It reports what did *not* agree rather than quietly picking, so a split shows up
196
+ as a question instead of a silent decision.
197
+
198
+ ```bash
199
+ retake routine init # or start from Retake's defaults instead
200
+ retake routine voices # hear the named voices once, then stop discussing speed
201
+ retake routine set voice neutral
202
+ retake routine set rate -8 # too fast, once, for every video that follows
203
+ retake routine # what every recording here will look and sound like
204
+ ```
205
+
206
+ Three styles, because a style is a shape and not a theme:
207
+
208
+ | Style | Output | Captions |
209
+ |---|---|---|
210
+ | `showcase` | 1920×1080 | the app inset on your own colour, with a shadow |
211
+ | `fullscreen` | 1920×1080 | the app fills the frame, nothing around it |
212
+ | `mobile` | 1080×1920 | inset the same way, for shorts and reels |
213
+
214
+ **Proving they match.** A standard nobody measures is a hope.
215
+
216
+ ```bash
217
+ retake routine check # measures the finished files, not the manifests
218
+ ```
219
+
220
+ It reads back size, frame rate, whether there is a voice and how loud it is, and
221
+ judges each video against the standard *and* against the others — because two
222
+ videos can agree with each other and both be wrong (a batch whose final render
223
+ failed leaves matching previews at the wrong frame rate).
224
+
225
+ Three things it cannot see, which the earlier steps catch instead: a key Retake
226
+ does not know (strict manifests reject it), a script copied from another demo
227
+ (`validate` compares the narration against that demo's own captions), and data
228
+ the app repopulated mid-take (stub the sync so nothing outside the demo can
229
+ change what is on screen).
230
+
231
+ **Start from a north star.** One approved video that every cut is held to:
232
+
233
+ ```bash
234
+ retake routine reference "~/Videos/the-one-we-liked.mp4"
235
+ ```
236
+
237
+ Before anything records, it says what this routine cannot reproduce — "the
238
+ reference is narrated, the routine is silent", "the routine uses Retake's own
239
+ card, not a branded one" — so a take is never the thing that discovers it.
240
+ After, `retake routine check` scores each cut against it: the opening card, the
241
+ closing card and the frame around the app, as structural similarity, plus
242
+ size, frame rate and how loud the voice is.
243
+
244
+ Two pieces exist so a north star *can* be reproduced: a branded `card` (the
245
+ product's mark, typeface, ground, ink and corner circles, drawn by Retake
246
+ instead of added afterwards), and narration with `align: scenes` — the script
247
+ read once so it sounds like a person, then each paragraph placed on its
248
+ narrated scene. A line that would run into the next scene is reported, never
249
+ sped up.
250
+
251
+ **Recording a batch.** `demos/plan.json` is the queue — names and one-sentence
252
+ stories, written by whoever has the ideas. A routine never invents one.
253
+
254
+ ```bash
255
+ retake routine run # everything not done yet
256
+ retake routine run -n 3 # three videos, then stop
257
+ retake routine run --only hero # re-record one
258
+ ```
259
+
260
+ **Letting it run itself.** Once a routine has a plan and a north star, nobody
261
+ needs to be at the keyboard:
262
+
263
+ ```bash
264
+ retake routine schedule weekdays 09:00 # the next drafted video, every weekday morning
265
+ retake routine schedule # what is scheduled, and whether the machine holds it
266
+ retake routine schedule off
267
+ ```
268
+
269
+ Each run brings the app up isolated, records one video, judges it against the
270
+ north star and delivers it only if it matches — then a notification says how
271
+ it went. launchd wakes it on a Mac, cron on Linux; nothing of Retake's stays
272
+ running. A story with no manifest yet waits instead of failing, so an
273
+ undrafted idea never costs a morning.
274
+
275
+ Each item is stamped, dry-run, recorded as a preview and finished at full
276
+ quality, with progress written to `plan.json` as it goes — so an interrupted
277
+ batch resumes where it stopped, and one broken selector does not cost the other
278
+ four videos. Two failures in a row stops the batch and asks whether the app is
279
+ actually running.
280
+
281
+ **Two standards, two products.** `retake routine new techtutor-vn`, then
282
+ `retake routine set voice vi-VN-HoaiMyNeural --as techtutor-vn`. The language
283
+ rides on the voice, so there is no second setting that could disagree with the
284
+ first. `retake routine use <name>` switches; `--as <name>` is a one-off.
285
+
286
+ **What a routine will not do:** turn narration on. That needs a script and a
287
+ person who has heard it — the routine only says which voice reads it, and how
288
+ fast, once somebody has written one.
289
+
103
290
  ## Demo-as-code
104
291
 
105
292
  ```yaml
@@ -129,7 +316,7 @@ outputs:
129
316
  thumbnail: { scene: result }
130
317
  ```
131
318
 
132
- The full template is [`demos/example.yaml`](demos/example.yaml); `retake init` copies it into a new workspace.
319
+ On a new workspace, `retake ui` opens the first-demo flow and writes the manifest from your answers.
133
320
 
134
321
  **Steps** — `click`, `type`, `fill`, `select`, `hover`, `drag`, `scroll`, `keyboard`, `navigate`, `wait`, `waitFor`, `evaluate`, `upload`, `download`, `screenshot`, `scene`. Every step takes `pauseAfter`, `waitFor`, `timeout`, `secret`. Targets are a selector, or a point — `{x, y}` or `{selector, dx, dy}` — for canvases and SVG editors. `${VAR}` expands from `.env` and fails loudly when unset.
135
322
 
@@ -163,37 +350,34 @@ Think in the publishing format, not the browser window.
163
350
 
164
351
  Most real demos start on the far side of a sign-in. You never put a password anywhere an agent can read it.
165
352
 
166
- **With the window open** (`retake ui`): when the agent sees the app needs a login, a small form appears in the window asking for a demo account by name — `APP_USER`, `APP_PASSWORD`. You type them; they are written to one local file, the workspace `.env` (readable by you only), and never sent to the agent, to any server, or to anyone. The agent learns only that the names are set.
353
+ **Save a login once per site.** When the agent sees the app needs a login, a small form appears in the window: *Sign in to app.example.com*. Type a demo account's username and password; Retake keeps them in your Mac's Keychain (the system keyring on Linux), never in a file the agent can read, and never sends them anywhere. Every demo of that site — in any workspace — signs in with it from then on, and signs in again by itself whenever its session runs out.
167
354
 
168
- **Without the window:** the agent relays one line — run this in your workspace and type the values (hidden):
355
+ **Without the window**, the agent relays one line — run it and type the login (hidden):
169
356
 
170
357
  ```bash
171
- retake secret APP_USER APP_PASSWORD
358
+ retake login https://app.example.com
172
359
  ```
173
360
 
174
- **Then:** the login runs *before the camera* and is trimmed off the front; the video starts signed in. The session is saved to `.auth/`, so later takes skip the login. A wrong password stops the take instead of recording a logged-out one.
175
-
176
- **Two-factor.** Authenticator apps: add the enrolment secret as `APP_TOTP_SECRET` and the manifest uses `${TOTP:APP_TOTP_SECRET}` — Retake computes the current code when it fills the field. SMS codes, SSO, captchas — anything only a person can do:
361
+ `retake logins` lists the sites with a saved login (names only); `retake login <url> --forget` removes one.
177
362
 
178
- ```bash
179
- retake signin demos/<name>.yaml
180
- ```
363
+ **Then:** the login runs *before the camera* and is trimmed off the front; the video starts signed in. The session is saved to `.auth/`, so later takes skip the login. A wrong password stops the take instead of recording a logged-out one.
181
364
 
182
- opens a real browser; you log in by hand, press Enter, and Retake keeps the session (not the password) for every later take.
365
+ **Two-factor.** If the site asks for an authenticator code, choose *It asks for a code* and paste the setup key; Retake computes the current code when it fills the field. SMS codes, SSO, captchas — anything only a person can do — choose *Sign in by hand*: a real browser opens, you sign in, press Done, and Retake keeps the session (not the password). From a terminal: `retake login <url> --by-hand`.
183
366
 
184
367
  What the agent writes:
185
368
 
186
369
  ```yaml
187
370
  auth:
188
- storageState: .auth/myapp.json # session saved here, reused until stale
189
- maxAgeHours: 8
371
+ storageState: ../.auth/app.example.com.json # the site's session, reused until stale
190
372
  setup:
191
- - { action: fill, selector: "#user", text: "${APP_USER}" }
192
- - { action: fill, selector: "#password", text: "${APP_PASSWORD}", secret: true }
193
- - { action: fill, selector: "#code", text: "${TOTP:APP_TOTP_SECRET}", secret: true } # only if asked
373
+ - { action: fill, selector: "#user", text: "${LOGIN_USER}", secret: true }
374
+ - { action: fill, selector: "#password", text: "${LOGIN_PASSWORD}", secret: true }
375
+ - { action: fill, selector: "#code", text: "${TOTP:LOGIN_TOTP}", secret: true } # only if asked
194
376
  - { action: click, selector: "button[type=submit]" }
195
377
  ```
196
378
 
379
+ A value of the same name in the workspace `.env` still wins, so demos written with `${APP_USER}`-style names and `retake secret` keep working.
380
+
197
381
  `secret: true` keeps a value out of the terminal, the window and the proof log — but not out of the video, which is why `validate` warns when a secret step is in `steps` rather than `auth.setup`. Use a demo account: the output is a video you may publish. Apps that keep their session in IndexedDB (Firebase) can't be restored from a saved session; put the login under plain `setup` instead.
198
382
 
199
383
  ## Limits worth knowing
Binary file
Binary file
Binary file
@@ -0,0 +1,66 @@
1
+ export function proofMatches(spec, snapshot) {
2
+ switch (spec.state) {
3
+ case "visible": return snapshot.found && snapshot.visible;
4
+ case "hidden": return !snapshot.found || !snapshot.visible;
5
+ case "active":
6
+ return snapshot.found && snapshot.visible && (snapshot.classTokens.some((token) => /(^|[-_])(active|selected|current|checked)([-_]|$)/i.test(token)) ||
7
+ snapshot.attributes["aria-selected"] === "true" ||
8
+ snapshot.attributes["aria-pressed"] === "true" ||
9
+ snapshot.attributes["data-state"] === "active" ||
10
+ snapshot.checked);
11
+ case "checked": return snapshot.found && snapshot.checked;
12
+ case "text": return snapshot.found && snapshot.text.includes(spec.value ?? "");
13
+ case "attribute": return snapshot.found && snapshot.attributes[spec.attribute ?? ""] === spec.value;
14
+ }
15
+ }
16
+ export function assessActionProof(spec, before, after) {
17
+ if (spec.change && proofMatches(spec, before)) {
18
+ return { ok: false, reason: `proof already satisfied before the action: ${describeProof(spec)}; choose an action that visibly changes state` };
19
+ }
20
+ if (!proofMatches(spec, after)) {
21
+ return { ok: false, reason: `proof not satisfied after the action: ${describeProof(spec)}` };
22
+ }
23
+ return { ok: true, reason: "" };
24
+ }
25
+ export async function snapshotActionProof(page, spec) {
26
+ const locator = page.locator(spec.selector).first();
27
+ const found = await locator.count().then((count) => count > 0).catch(() => false);
28
+ if (!found)
29
+ return emptySnapshot();
30
+ const visible = await locator.isVisible().catch(() => false);
31
+ return locator.evaluate((element) => {
32
+ const input = element;
33
+ const attributes = {};
34
+ for (const name of ["aria-selected", "aria-pressed", "data-state"])
35
+ attributes[name] = element.getAttribute(name);
36
+ for (const attribute of Array.from(element.attributes))
37
+ attributes[attribute.name] = attribute.value;
38
+ return {
39
+ found: true,
40
+ visible: true,
41
+ classTokens: Array.from(element.classList),
42
+ attributes,
43
+ text: (element.textContent ?? "").replace(/\s+/g, " ").trim(),
44
+ checked: "checked" in input ? Boolean(input.checked) : false,
45
+ };
46
+ }).then((snapshot) => ({ ...snapshot, visible })).catch(() => ({ ...emptySnapshot(), found, visible }));
47
+ }
48
+ export async function waitForActionProof(page, spec) {
49
+ const deadline = Date.now() + spec.timeout;
50
+ let latest = await snapshotActionProof(page, spec);
51
+ while (!proofMatches(spec, latest) && Date.now() < deadline) {
52
+ await page.waitForTimeout(100);
53
+ latest = await snapshotActionProof(page, spec);
54
+ }
55
+ return latest;
56
+ }
57
+ export function describeProof(spec) {
58
+ if (spec.state === "text")
59
+ return `${spec.selector} contains text ${JSON.stringify(spec.value ?? "")}`;
60
+ if (spec.state === "attribute")
61
+ return `${spec.selector} has ${spec.attribute}=${JSON.stringify(spec.value ?? "")}`;
62
+ return `${spec.selector} is ${spec.state}`;
63
+ }
64
+ function emptySnapshot() {
65
+ return { found: false, visible: false, classTokens: [], attributes: {}, text: "", checked: false };
66
+ }
@@ -0,0 +1,64 @@
1
+ /** Retake's browser lives in Retake's own folder.
2
+
3
+ Playwright keeps one browser cache for every project on the machine, and
4
+ any project's `playwright install` sweeps out the browsers it decides
5
+ nobody uses — Retake's included. Recording then stopped with "run npx
6
+ playwright install", for a reason the person never caused and could not
7
+ see. So Retake keeps its browser where no other project looks, and fetches
8
+ it again by itself if it is ever missing.
9
+
10
+ Import this module before anything that loads playwright: Playwright reads
11
+ where its browsers are once, when it loads. An explicit
12
+ PLAYWRIGHT_BROWSERS_PATH (CI sets one) is left alone. */
13
+ import fs from "node:fs";
14
+ import os from "node:os";
15
+ import path from "node:path";
16
+ import { spawnSync } from "node:child_process";
17
+ import { createRequire } from "node:module";
18
+ function cacheHome() {
19
+ if (process.platform === "darwin")
20
+ return path.join(os.homedir(), "Library", "Caches");
21
+ if (process.platform === "win32")
22
+ return process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
23
+ return process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
24
+ }
25
+ export const BROWSERS = process.env.PLAYWRIGHT_BROWSERS_PATH || path.join(cacheHome(), "retake", "browsers");
26
+ process.env.PLAYWRIGHT_BROWSERS_PATH = BROWSERS;
27
+ const require = createRequire(import.meta.url);
28
+ /** The browser Retake's Playwright wants, if it is on disk. */
29
+ export function chromiumPath() {
30
+ try {
31
+ const { chromium } = require("playwright");
32
+ const p = chromium.executablePath();
33
+ return fs.existsSync(p) ? p : null;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ /** `npx playwright install chromium`, from wherever playwright actually is. */
40
+ export function installChromium(say = (l) => process.stderr.write(l + "\n")) {
41
+ let cli;
42
+ try {
43
+ cli = path.join(path.dirname(require.resolve("playwright/package.json")), "cli.js");
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ say("… downloading Retake's browser (one time, ~150 MB)");
49
+ const r = spawnSync(process.execPath, [cli, "install", "chromium"], { stdio: ["ignore", "ignore", "inherit"], env: process.env });
50
+ return r.status === 0;
51
+ }
52
+ const missing = (e) => /Executable doesn't exist|playwright install/i.test(String(e?.message ?? e));
53
+ /** chromium.launch, but a missing browser is fetched once instead of stopping the take. */
54
+ export async function launchChromium(options) {
55
+ const { chromium } = await import("playwright");
56
+ try {
57
+ return await chromium.launch(options);
58
+ }
59
+ catch (e) {
60
+ if (!missing(e) || !installChromium())
61
+ throw e;
62
+ return chromium.launch(options);
63
+ }
64
+ }