retake-demos 0.13.2 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +4 -6
- package/AGENTS.md +69 -19
- package/README.md +203 -19
- package/assets/voice-samples/aiden.mp4 +0 -0
- package/assets/voice-samples/edge.mp4 +0 -0
- package/assets/voice-samples/kokoro.mp4 +0 -0
- package/dist/action-proof.js +66 -0
- package/dist/browser-home.js +64 -0
- package/dist/cards.js +103 -5
- package/dist/cli.js +965 -53
- package/dist/describe.js +3 -3
- package/dist/dryrun.js +56 -13
- package/dist/ext/golden.js +241 -0
- package/dist/hold.js +75 -0
- package/dist/logins.js +194 -0
- package/dist/manifest.js +239 -25
- package/dist/media-bin.js +23 -0
- package/dist/operator/run.js +82 -7
- package/dist/operator/tools.js +311 -42
- package/dist/pace.js +35 -3
- package/dist/paths.js +4 -1
- package/dist/presets.js +8 -0
- package/dist/preview.js +197 -0
- package/dist/record.js +357 -40
- package/dist/recording-env.js +201 -0
- package/dist/render.js +502 -88
- package/dist/routine.js +839 -0
- package/dist/schedule.js +169 -0
- package/dist/self-repair.js +64 -0
- package/dist/setup.js +728 -0
- package/dist/signin.js +46 -0
- package/dist/ui/chat.html +920 -11
- package/dist/ui/server.js +341 -23
- package/dist/voice.js +171 -4
- package/docs/stitch.md +38 -0
- package/llms.txt +0 -1
- package/package.json +12 -8
- package/scripts/patch-testreel.mjs +87 -5
- package/site/releases.json +28 -0
- package/skill/SKILL.md +86 -50
- package/demos/example.yaml +0 -43
- package/dist/ui/guide.html +0 -132
package/.env.example
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
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
|
-
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
- **Put the sign-in under `auth.setup`** with `${
|
|
198
|
-
and `secret: true`, plus
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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.
|
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
|
|
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
|
-
|
|
47
|
+
Start `retake ui` and open http://localhost:4310. On an empty workspace, Retake asks only for:
|
|
48
48
|
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
|
355
|
+
**Without the window**, the agent relays one line — run it and type the login (hidden):
|
|
169
356
|
|
|
170
357
|
```bash
|
|
171
|
-
retake
|
|
358
|
+
retake login https://app.example.com
|
|
172
359
|
```
|
|
173
360
|
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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: "${
|
|
192
|
-
- { action: fill, selector: "#password", text: "${
|
|
193
|
-
- { action: fill, selector: "#code", text: "${TOTP:
|
|
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
|
+
}
|