retake-demos 0.14.0 → 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/dist/cli.js +28 -4
- package/dist/media-bin.js +7 -8
- package/dist/operator/tools.js +1 -0
- package/dist/record.js +37 -1
- package/dist/self-repair.js +64 -0
- package/dist/setup.js +4 -1
- package/dist/ui/server.js +1 -0
- package/dist/voice.js +26 -2
- package/package.json +6 -6
- package/site/releases.json +13 -0
package/dist/cli.js
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
* installed from npm, dist/cli.js is the `retake` binary. Same file.
|
|
34
34
|
*/
|
|
35
35
|
import { BROWSERS, chromiumPath, installChromium, launchChromium } from "./browser-home.js";
|
|
36
|
+
import { bundledFfmpeg, recorderPatched } from "./self-repair.js";
|
|
36
37
|
import fs from "node:fs";
|
|
37
38
|
import path from "node:path";
|
|
38
39
|
import { createRequire } from "node:module";
|
|
@@ -807,11 +808,14 @@ program
|
|
|
807
808
|
try {
|
|
808
809
|
const require = createRequire(import.meta.url);
|
|
809
810
|
const ff = require("ffmpeg-static");
|
|
810
|
-
fs.existsSync(ff) ? ok(`ffmpeg ${ff}`) :
|
|
811
|
+
fs.existsSync(ff) ? ok(`ffmpeg ${ff}`) : say("· ffmpeg not downloaded yet — the first render fetches it (or run: retake install)");
|
|
811
812
|
}
|
|
812
813
|
catch {
|
|
813
814
|
no("ffmpeg-static not found — reinstall retake-demos");
|
|
814
815
|
}
|
|
816
|
+
const patched = recorderPatched();
|
|
817
|
+
if (patched === false)
|
|
818
|
+
no("Retake's recorder patch is not applied — run any retake command once and it applies itself");
|
|
815
819
|
const which = (bin) => spawnSync("which", [bin], { encoding: "utf8" }).stdout.trim();
|
|
816
820
|
say(which("gifski") ? `✓ gifski (better GIFs)` : `· gifski not installed — GIFs fall back to ffmpeg (brew install gifski)`);
|
|
817
821
|
say(which("claude") ? `✓ claude CLI — \`retake install\` can register the tools` : `· claude CLI not found — paste the config from \`retake agent\` into your agent`);
|
|
@@ -831,18 +835,38 @@ program
|
|
|
831
835
|
// 1. the workspace: demos/ and outputs/ live where the command was run
|
|
832
836
|
initWorkspace(process.cwd(), say);
|
|
833
837
|
// 2. the browser
|
|
838
|
+
if (bundledFfmpeg())
|
|
839
|
+
say("✓ ffmpeg present");
|
|
840
|
+
else
|
|
841
|
+
say("✗ could not get ffmpeg — install it once (brew install ffmpeg) and try again");
|
|
834
842
|
if (chromiumPath())
|
|
835
843
|
say("✓ chromium present");
|
|
836
844
|
else if (!installChromium(say))
|
|
837
845
|
say("✗ could not download Chromium — run `npx playwright install chromium` and try again");
|
|
838
846
|
// 3. the tools, registered with Claude Code for every project
|
|
839
847
|
const server = mcpServer(o.ui);
|
|
848
|
+
// Running install again (an upgrade, a new workspace) finds the tools
|
|
849
|
+
// already registered — replace that registration rather than calling it
|
|
850
|
+
// a failure, so it points at this install and this folder.
|
|
851
|
+
const add = () => execFileSync("claude", ["mcp", "add-json", "retake", JSON.stringify(server), "--scope", "user"], { stdio: "pipe", encoding: "utf8" });
|
|
840
852
|
try {
|
|
841
|
-
|
|
853
|
+
add();
|
|
842
854
|
say("✓ Claude Code: retake tools registered (user scope)");
|
|
843
855
|
}
|
|
844
|
-
catch {
|
|
845
|
-
|
|
856
|
+
catch (e) {
|
|
857
|
+
const why = String(e.stderr ?? e.message ?? "");
|
|
858
|
+
let fixed = false;
|
|
859
|
+
if (/already exists/i.test(why)) {
|
|
860
|
+
try {
|
|
861
|
+
execFileSync("claude", ["mcp", "remove", "retake", "--scope", "user"], { stdio: "pipe" });
|
|
862
|
+
add();
|
|
863
|
+
fixed = true;
|
|
864
|
+
}
|
|
865
|
+
catch { /* fall through */ }
|
|
866
|
+
}
|
|
867
|
+
say(fixed ? "✓ Claude Code: retake tools re-registered for this folder (user scope)"
|
|
868
|
+
: /ENOENT/.test(why) ? "✗ Claude Code CLI not found — run `retake agent` and paste the config yourself"
|
|
869
|
+
: `✗ Claude Code refused to register the tools${why.trim() ? ` (${why.trim().split("\n").pop()})` : ""} — run \`retake agent\` and paste the config yourself`);
|
|
846
870
|
}
|
|
847
871
|
// 4. the skill: when to reach for the tools, and in what order
|
|
848
872
|
const skillSrc = path.join(PKG_ROOT, "skill", "SKILL.md");
|
package/dist/media-bin.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
1
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import {
|
|
4
|
-
const require = createRequire(import.meta.url);
|
|
2
|
+
import { bundledFfmpeg } from "./self-repair.js";
|
|
5
3
|
let cachedBin;
|
|
6
4
|
/** Resolve the bundled media binary first, then fall back to PATH. */
|
|
7
5
|
export function ffmpegBin() {
|
|
@@ -11,14 +9,15 @@ export function ffmpegBin() {
|
|
|
11
9
|
execFileSync(process.env.FFMPEG_BIN, ["-version"], { stdio: "ignore" });
|
|
12
10
|
return (cachedBin = process.env.FFMPEG_BIN);
|
|
13
11
|
}
|
|
12
|
+
// The bundled copy — fetched now if npm skipped its install script.
|
|
13
|
+
const bundled = bundledFfmpeg();
|
|
14
|
+
if (bundled)
|
|
15
|
+
return (cachedBin = bundled);
|
|
14
16
|
try {
|
|
15
|
-
|
|
16
|
-
if (bundled && fs.existsSync(bundled))
|
|
17
|
-
return (cachedBin = bundled);
|
|
17
|
+
execFileSync("ffmpeg", ["-version"], { stdio: "ignore" });
|
|
18
18
|
}
|
|
19
19
|
catch {
|
|
20
|
-
|
|
20
|
+
throw new Error("Retake needs ffmpeg and could not get its own copy — install it once (`brew install ffmpeg` on a Mac), or reinstall with `npm install -g --allow-scripts=retake-demos,ffmpeg-static retake-demos`");
|
|
21
21
|
}
|
|
22
|
-
execFileSync("ffmpeg", ["-version"], { stdio: "ignore" });
|
|
23
22
|
return (cachedBin = "ffmpeg");
|
|
24
23
|
}
|
package/dist/operator/tools.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* happening in plain sentences.
|
|
13
13
|
*/
|
|
14
14
|
import "../browser-home.js";
|
|
15
|
+
import "../self-repair.js";
|
|
15
16
|
import { hasLogin, loginStoreName, readLogin, siteOf } from "../logins.js";
|
|
16
17
|
import { sessionAgeHours, sessionPathFor } from "../signin.js";
|
|
17
18
|
import fs from "node:fs";
|
package/dist/record.js
CHANGED
|
@@ -122,6 +122,22 @@ export function persistentCssScript(css) {
|
|
|
122
122
|
}
|
|
123
123
|
})();`;
|
|
124
124
|
}
|
|
125
|
+
/** Present in the DOM is not the same as on screen. A page whose stylesheet
|
|
126
|
+
or web font is still on its way has every element the manifest waits for
|
|
127
|
+
and paints nothing: an agent's demo of a public site opened on two seconds
|
|
128
|
+
of blank page, and every check passed. So before the camera's clock
|
|
129
|
+
starts, let the page finish loading what it draws with — bounded, because
|
|
130
|
+
a busy site may never be "done", and the manifest's own gate already
|
|
131
|
+
proved the app is there. */
|
|
132
|
+
export async function untilPainted(page) {
|
|
133
|
+
await page.waitForLoadState("load", { timeout: 8000 }).catch(() => { });
|
|
134
|
+
await page.evaluate(() => Promise.race([
|
|
135
|
+
document.fonts?.ready,
|
|
136
|
+
new Promise((ok) => setTimeout(ok, 4000)),
|
|
137
|
+
])).catch(() => { });
|
|
138
|
+
// Two frames: the first after fonts swap in, the second actually drawn.
|
|
139
|
+
await page.evaluate(() => new Promise((ok) => requestAnimationFrame(() => requestAnimationFrame(() => ok(null))))).catch(() => { });
|
|
140
|
+
}
|
|
125
141
|
/** The session a take should start from, and whether it is still good.
|
|
126
142
|
Shared so `dry` starts signed in exactly when `run` would — a dry run
|
|
127
143
|
against a logged-out app "proving" a manifest is a wasted take waiting
|
|
@@ -626,6 +642,13 @@ export async function record(m, opts) {
|
|
|
626
642
|
// --- setup phase (still inside the video; trimmed later by `trimBefore`) ---
|
|
627
643
|
// Document first; idle only if it comes quickly. Busy public sites never
|
|
628
644
|
// go network-idle, and the manifest's waitForSelector is the real gate.
|
|
645
|
+
// The video starts at the first frame the tab paints. Left to itself that
|
|
646
|
+
// is somewhere between the empty tab and the site's first paint — on a
|
|
647
|
+
// real site, measured, up to 1.5s after the moment we took for it, which
|
|
648
|
+
// left blank white frames at the front and every cursor move, scene cut
|
|
649
|
+
// and caption that late. So paint the tab once, now, and take THAT as
|
|
650
|
+
// the start: measured within ~0.1s on local and public sites alike.
|
|
651
|
+
const paintedAt = await paintFirstFrame(page);
|
|
629
652
|
await page.goto(expandEnv(m.url), { waitUntil: "domcontentloaded", timeout: 60000 });
|
|
630
653
|
// The manifest's readiness selectors are authoritative. Busy public apps
|
|
631
654
|
// may never become network-idle; paying a fixed 8s before every take made
|
|
@@ -649,10 +672,11 @@ export async function record(m, opts) {
|
|
|
649
672
|
}
|
|
650
673
|
}
|
|
651
674
|
await gateOnApp(page, m, opts.outDir);
|
|
675
|
+
await untilPainted(page);
|
|
652
676
|
// Playwright's video begins at the first *painted* frame, not at newPage().
|
|
653
677
|
// On a dev server that can be seconds later; if testreel's cursor overlay is
|
|
654
678
|
// told the wrong start it draws every cursor move that many seconds late.
|
|
655
|
-
const videoStartedAt = await firstPaintWallClock(page);
|
|
679
|
+
const videoStartedAt = paintedAt ?? await firstPaintWallClock(page);
|
|
656
680
|
t0 = videoStartedAt;
|
|
657
681
|
// Idle-hide fades are nested if()s in testreel's ffmpeg alpha expression;
|
|
658
682
|
// past roughly sixty steps the nesting exceeds ffmpeg's parser and the
|
|
@@ -1542,6 +1566,18 @@ export function describe(step) {
|
|
|
1542
1566
|
return `scene: ${step.label}`;
|
|
1543
1567
|
}
|
|
1544
1568
|
}
|
|
1569
|
+
/** Paint the empty tab and return the wall-clock ms of that paint — the
|
|
1570
|
+
video's first frame. Null if the page would not paint (then the old
|
|
1571
|
+
estimate stands). It happens during setup, so it is trimmed away. */
|
|
1572
|
+
export async function paintFirstFrame(page) {
|
|
1573
|
+
try {
|
|
1574
|
+
await page.setContent('<html style="background:#fff;height:100%"><body></body></html>');
|
|
1575
|
+
return Math.round(await page.evaluate(() => new Promise((ok) => requestAnimationFrame(() => requestAnimationFrame(() => ok(performance.timeOrigin + performance.now()))))));
|
|
1576
|
+
}
|
|
1577
|
+
catch {
|
|
1578
|
+
return null;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1545
1581
|
/** Wall-clock ms when the recorded page's document committed — empirically
|
|
1546
1582
|
where Playwright's first video frame lands (falls back to now). */
|
|
1547
1583
|
async function firstPaintWallClock(page) {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** What npm was supposed to do at install, done at start if it did not.
|
|
2
|
+
|
|
3
|
+
Two of Retake's parts arrive by install script: the patch that lets the
|
|
4
|
+
recorder draw the cursor right (scripts/patch-testreel.mjs, Retake's own
|
|
5
|
+
postinstall) and ffmpeg itself (ffmpeg-static downloads its binary at
|
|
6
|
+
install). npm has begun asking before it runs install scripts for global
|
|
7
|
+
packages, so a person can end up with neither — and a recorder that drops
|
|
8
|
+
the cursor, or a render that cannot start, for a reason they never saw.
|
|
9
|
+
|
|
10
|
+
Import this before anything that loads testreel: the patch has to be on
|
|
11
|
+
disk before the recorder is read. Both repairs are idempotent and cheap
|
|
12
|
+
when nothing is missing (a file read, a stat). */
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
import { createRequire } from "node:module";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const say = (l) => process.stderr.write(l + "\n");
|
|
20
|
+
/** The marks the patch leaves. All present → patched. */
|
|
21
|
+
const MARKS = ["/*retake:flat-cursor-v2*/", "/*retake:coordinate-scale*/"];
|
|
22
|
+
export function recorderPatched() {
|
|
23
|
+
try {
|
|
24
|
+
const src = fs.readFileSync(path.join(path.dirname(require.resolve("testreel")), "index.js"), "utf8");
|
|
25
|
+
return MARKS.every((m) => src.includes(m));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function patchRecorder() {
|
|
32
|
+
if (recorderPatched() !== false)
|
|
33
|
+
return;
|
|
34
|
+
const script = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "scripts", "patch-testreel.mjs");
|
|
35
|
+
if (!fs.existsSync(script))
|
|
36
|
+
return;
|
|
37
|
+
const r = spawnSync(process.execPath, [script], { encoding: "utf8" });
|
|
38
|
+
if (recorderPatched())
|
|
39
|
+
say("✓ applied Retake's recorder patch (npm skipped it at install)");
|
|
40
|
+
else
|
|
41
|
+
say(`· could not apply Retake's recorder patch — long demos may lose the cursor. ${r.stdout?.trim().split("\n").pop() ?? ""}`);
|
|
42
|
+
}
|
|
43
|
+
/** ffmpeg-static's binary, fetched now if its install script never ran.
|
|
44
|
+
Returns its path, or null when there is no bundled copy to be had. */
|
|
45
|
+
export function bundledFfmpeg() {
|
|
46
|
+
let bin = null;
|
|
47
|
+
try {
|
|
48
|
+
bin = require("ffmpeg-static");
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (!bin)
|
|
54
|
+
return null;
|
|
55
|
+
if (fs.existsSync(bin))
|
|
56
|
+
return bin;
|
|
57
|
+
const installer = path.join(path.dirname(require.resolve("ffmpeg-static/package.json")), "install.js");
|
|
58
|
+
if (!fs.existsSync(installer))
|
|
59
|
+
return null;
|
|
60
|
+
say("… downloading ffmpeg (one time, ~45 MB — npm skipped it at install)");
|
|
61
|
+
spawnSync(process.execPath, [installer], { cwd: path.dirname(installer), stdio: ["ignore", "ignore", "inherit"] });
|
|
62
|
+
return fs.existsSync(bin) ? bin : null;
|
|
63
|
+
}
|
|
64
|
+
patchRecorder();
|
package/dist/setup.js
CHANGED
|
@@ -503,7 +503,10 @@ export async function readBrand(url, outBase, o = {}) {
|
|
|
503
503
|
// school's site it was a cartoon character, not the TechBash wordmark.
|
|
504
504
|
// A wordmark set in text is captured as a picture, background removed.
|
|
505
505
|
const head = page.locator(HEADER_LOGO).first();
|
|
506
|
-
|
|
506
|
+
// No header logo is common, and boundingBox() on nothing waits out
|
|
507
|
+
// Playwright's 30 seconds — a first-run screen that sat on "Reading your
|
|
508
|
+
// site…" for half a minute. Ask whether it exists first.
|
|
509
|
+
const box = (await head.count().catch(() => 0)) ? await head.boundingBox({ timeout: 2000 }).catch(() => null) : null;
|
|
507
510
|
if (box && box.width >= 40 && box.height >= 14 && box.height <= 140 && box.y < 200) {
|
|
508
511
|
const inner = await head.evaluate(`(el) => { const i = el.querySelector('img[src$=".svg"], img'); const s = el.querySelector('svg'); return { img: i ? i.src : null, svg: s && !i ? s.outerHTML : null }; }`).catch(() => null);
|
|
509
512
|
fs.mkdirSync(path.dirname(outBase), { recursive: true });
|
package/dist/ui/server.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* GET /out/:name/<file> artifacts (mp4/gif/png/md)
|
|
23
23
|
*/
|
|
24
24
|
import "../browser-home.js";
|
|
25
|
+
import "../self-repair.js";
|
|
25
26
|
import { hasLogin, loginStoreName, saveLogin, siteOf } from "../logins.js";
|
|
26
27
|
import { openHandSignIn, sessionAgeHours, sessionPathFor } from "../signin.js";
|
|
27
28
|
import http from "node:http";
|
package/dist/voice.js
CHANGED
|
@@ -30,6 +30,30 @@ function engine() {
|
|
|
30
30
|
return { cmd: "python3", args: ["-m", "edge_tts"] };
|
|
31
31
|
throw new Error("voiceover needs edge-tts — install it once with `pipx install edge-tts` (or `pip install edge-tts`), then re-render");
|
|
32
32
|
}
|
|
33
|
+
/** Run edge-tts, and when it fails say why in a sentence a person can act on —
|
|
34
|
+
not the whole command line with the script pasted into it. The usual
|
|
35
|
+
failure is Microsoft's service refusing an out-of-date edge-tts (a 403):
|
|
36
|
+
it worked yesterday, it stops today, and upgrading fixes it. */
|
|
37
|
+
export function edgeFailure(stderr, cmd) {
|
|
38
|
+
const upgrade = cmd === "python3" ? "`pip install -U edge-tts`" : "`pipx upgrade edge-tts` (or `pip install -U edge-tts`)";
|
|
39
|
+
if (/\b403\b|WSServerHandshakeError/.test(stderr))
|
|
40
|
+
return `voiceover: Microsoft's voice service refused the request (403). That almost always means edge-tts is out of date — run ${upgrade}, then re-render. If it is already current, check that this computer's clock is right.`;
|
|
41
|
+
if (/getaddrinfo|ClientConnectorError|Cannot connect|Network is unreachable|TimeoutError/i.test(stderr))
|
|
42
|
+
return "voiceover: could not reach Microsoft's voice service — is the network up? (edge-tts is a network service)";
|
|
43
|
+
const last = stderr.trim().split("\n").filter(Boolean).pop() ?? "";
|
|
44
|
+
return `voiceover: edge-tts failed${last ? ` — ${last.slice(0, 200)}` : ""}`;
|
|
45
|
+
}
|
|
46
|
+
function runEdge(e, args, timeout) {
|
|
47
|
+
try {
|
|
48
|
+
execFileSync(e.cmd, [...e.args, ...args], { stdio: "pipe", timeout });
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
const x = err;
|
|
52
|
+
if (x.code === "ETIMEDOUT" || x.signal === "SIGTERM")
|
|
53
|
+
throw new Error("voiceover: Microsoft's voice service did not answer in time — try again in a minute");
|
|
54
|
+
throw new Error(edgeFailure(String(x.stderr ?? ""), e.cmd));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
33
57
|
export function synthesize(text, voice, out, ratePercent = 0) {
|
|
34
58
|
const cached = voiceCachePath(text, voice, ratePercent);
|
|
35
59
|
if (fs.existsSync(cached) && fs.statSync(cached).size >= 200) {
|
|
@@ -39,7 +63,7 @@ export function synthesize(text, voice, out, ratePercent = 0) {
|
|
|
39
63
|
const e = engine();
|
|
40
64
|
const temp = `${out}.tmp-${process.pid}`;
|
|
41
65
|
try {
|
|
42
|
-
|
|
66
|
+
runEdge(e, ["--voice", voice, "--rate", `${ratePercent >= 0 ? "+" : ""}${ratePercent}%`, "--text", text, "--write-media", temp], 60_000);
|
|
43
67
|
if (!fs.existsSync(temp) || fs.statSync(temp).size < 200)
|
|
44
68
|
throw new Error(`voiceover: nothing came back for “${text.slice(0, 40)}…” — is the network up? (edge-tts is a network service)`);
|
|
45
69
|
fs.mkdirSync(path.dirname(cached), { recursive: true });
|
|
@@ -87,7 +111,7 @@ export function synthesizeWithCues(text, voice, out, ratePercent = 0) {
|
|
|
87
111
|
const e = engine();
|
|
88
112
|
const temp = `${out}.tmp-${process.pid}`, srt = `${out}.srt`;
|
|
89
113
|
try {
|
|
90
|
-
|
|
114
|
+
runEdge(e, ["--voice", voice, "--rate", `${ratePercent >= 0 ? "+" : ""}${ratePercent}%`, "--text", text, "--write-media", temp, "--write-subtitles", srt], 120_000);
|
|
91
115
|
if (!fs.existsSync(temp) || fs.statSync(temp).size < 200)
|
|
92
116
|
throw new Error("voiceover: nothing came back — is the network up? (edge-tts is a network service)");
|
|
93
117
|
const cues = parseSrt(fs.readFileSync(srt, "utf8"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "retake-demos",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Demo-as-code for coding agents: a YAML manifest in, a product walkthrough video out. Rerun the demo instead of re-recording it.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"demo",
|
|
@@ -57,12 +57,12 @@
|
|
|
57
57
|
"site:test:browser": "node --test test/landing.test.mjs"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@modelcontextprotocol/sdk": "
|
|
61
|
-
"commander": "
|
|
62
|
-
"playwright": "
|
|
60
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
61
|
+
"commander": "14.0.3",
|
|
62
|
+
"playwright": "1.62.1",
|
|
63
63
|
"testreel": "0.2.0",
|
|
64
|
-
"yaml": "
|
|
65
|
-
"zod": "
|
|
64
|
+
"yaml": "2.9.0",
|
|
65
|
+
"zod": "4.4.3"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@types/node": "^22",
|
package/site/releases.json
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_readme": "What's new, written for the person using Retake — not for whoever wrote it. Both the landing page and the Retake window read THIS file; there is no second copy. Rules for adding one: say what changed for them, name no files, no flags they will not type, no words like refactor or regression. If a line only makes sense to someone who has read the source, it does not belong here. Newest first. Add an entry on every release that changes what people see.",
|
|
3
3
|
"releases": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.14.1",
|
|
6
|
+
"date": "2026-10-16",
|
|
7
|
+
"headline": "Videos of real websites open on the website, and installing just works",
|
|
8
|
+
"notes": [
|
|
9
|
+
"Recording a live website (not one on your own computer) used to start with a second or two of blank white, and the cursor, captions and scene cuts ran that much late. Retake now marks the true start of every recording, so the video opens on the page and everything lands on its moment.",
|
|
10
|
+
"Pasting your site now shows your colours in a few seconds. On sites without a logo in their header, Retake used to sit on “Reading your site…” for half a minute first.",
|
|
11
|
+
"Installing gets exactly the versions Retake was tested with, instead of whatever was newest that day.",
|
|
12
|
+
"If npm skips Retake's install steps — newer versions of npm ask before running them — Retake now finishes them itself the first time it runs: it fetches ffmpeg and applies its recorder fix.",
|
|
13
|
+
"Running the install command again (after an update, or in a new folder) now updates your agent's connection instead of reporting a failure.",
|
|
14
|
+
"When Microsoft's voice service refuses a voiceover, Retake now says what fixes it — usually updating the voice tool — instead of showing the whole command it ran."
|
|
15
|
+
]
|
|
16
|
+
},
|
|
4
17
|
{
|
|
5
18
|
"version": "0.14.0",
|
|
6
19
|
"date": "2026-10-14",
|