Skip to content

Commit ebd2b62

Browse files
Vitozhu04claude
andauthored
feat(cli): reverse start stops a Free account when the server blocks it
When /v1/hyperframes/reverse/start answers gate "block" for plan "free", `reverse start` saves the session, then exits non-zero. The error and its `fix` carry the upgrade link that credits the purchase to this run: the server's `upgrade_url`, else the plans page with `feature=reverse&rs=<session>`. Pro, unknown plans, `record`, and every error still go on. The reverse-any-video skill tells the agent to pass that exact link on and stop. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent 7d3c225 commit ebd2b62

6 files changed

Lines changed: 59 additions & 10 deletions

File tree

‎docs/packages/cli.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1518,9 +1518,9 @@ run started and saves the session HeyGen returns in the project's
15181518
with the render. It then prints the video's [`measure probe`](#measure) JSON,
15191519
with a `reverse` field: whether you are signed in, the plan (`free`, `pro`, or
15201520
`null` when unknown), the session, and on Free the `upgrade_url` that credits a
1521-
purchase to this run. It never stops the run: signed out,
1522-
offline, or on a server without the route, it skips the session and still
1523-
probes.
1521+
purchase to this run. It stops the run only when HeyGen blocks it for a Free
1522+
account: it then exits non-zero with that upgrade link. Signed out, offline, or
1523+
on a server without the route, it skips the session and still probes.
15241524

15251525
```bash
15261526
npx hyperframes reverse start ref.mp4 --dir my-rebuild

‎packages/cli/src/commands/reverse/start.test.ts‎

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,13 @@ vi.mock("../measure/probe.js", () => ({
3131
const { default: start } = await import("./start.js");
3232

3333
const SESSION = "rvs_0f8fad5b-d9cb-469f-a165-70867728950e";
34-
const recorded = (plan: "free" | "pro" | "unknown" = "pro", upgradeUrl?: string): ReverseStart => ({
34+
const recorded = (
35+
plan: "free" | "pro" | "unknown" = "pro",
36+
upgradeUrl?: string,
37+
gate: "record" | "block" = "record",
38+
): ReverseStart => ({
3539
recorded: true,
36-
session: { sessionId: SESSION, plan, gate: "record", ...(upgradeUrl ? { upgradeUrl } : {}) },
40+
session: { sessionId: SESSION, plan, gate, ...(upgradeUrl ? { upgradeUrl } : {}) },
3741
});
3842

3943
let root: string;
@@ -169,6 +173,32 @@ describe("reverse start", () => {
169173
expect(() => readFileSync(join(dir, "hyperframes.json"))).toThrow();
170174
});
171175

176+
it("stops a Free account the server blocks, with its upgrade link as the fix, after saving the session", async () => {
177+
const link = "https://hyperframes.studio/pricing?feature=reverse&rs=1";
178+
server.answer = recorded("free", link, "block");
179+
const dir = join(root, "rebuild");
180+
await expect(run("--dir", dir)).rejects.toThrow(CliRuntimeError);
181+
expect(output()).toMatchObject({ ok: false, error: expect.stringContaining(link), fix: link });
182+
expect(config(dir)).toMatchObject({ reverseSessionId: SESSION });
183+
});
184+
185+
it("builds the attributed plans link when a block names none", async () => {
186+
server.answer = recorded("free", undefined, "block");
187+
await expect(run()).rejects.toThrow(CliRuntimeError);
188+
const link = `https://plans.test/?feature=reverse&rs=${SESSION}`;
189+
expect(output()).toMatchObject({ ok: false, error: expect.stringContaining(link), fix: link });
190+
});
191+
192+
it.each<[string, ReverseStart]>([
193+
["a Pro account under block", recorded("pro", undefined, "block")],
194+
["an unknown plan under block", recorded("unknown", undefined, "block")],
195+
["a Free account under record", recorded("free")],
196+
])("goes on for %s", async (_label, answer) => {
197+
server.answer = answer;
198+
await run();
199+
expect(output()).toMatchObject({ ok: true, width: 1080, reverse: { recorded: true } });
200+
});
201+
172202
it("fails like measure probe when the video cannot be probed, before asking the server", async () => {
173203
probe.fail = true;
174204
await expect(run()).rejects.toThrow(CliRuntimeError);

‎packages/cli/src/commands/reverse/start.ts‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import { existsSync } from "node:fs";
22
import { defineCommand } from "../../utils/defineCommand.js";
33
import type { Example } from "../_examples.js";
44
import { jsonArg, runMeasure, videoArg } from "../../measure/command.js";
5-
import { reverseUpgradeUrl, startReverseSession, type ReverseStart } from "../../heygen/reverse.js";
5+
import { Fix } from "../../measure/media.js";
6+
import {
7+
reverseUpgradeUrl,
8+
startReverseSession,
9+
type ReverseSession,
10+
type ReverseStart,
11+
} from "../../heygen/reverse.js";
612
import {
713
projectConfigPath,
814
readProjectConfig,
@@ -41,11 +47,20 @@ function summary(started: ReverseStart, savedTo: string | undefined) {
4147
};
4248
}
4349

50+
function refusalWhenServerBlocksFree(session: ReverseSession): Fix | undefined {
51+
if (session.gate !== "block" || session.plan !== "free") return undefined;
52+
const link = reverseUpgradeUrl(session);
53+
return new Fix(
54+
`Reverse is part of HyperFrames Pro. Upgrade here, then run reverse start again: ${link}`,
55+
link,
56+
);
57+
}
58+
4459
export default defineCommand({
4560
meta: {
4661
name: "start",
4762
description:
48-
"Note a reverse run with HeyGen (signed in: saves its session in the project), then print the video's probe; never stops the run",
63+
"Note a reverse run with HeyGen (signed in: saves its session in the project), then print the video's probe; stops only when HeyGen blocks a Free account",
4964
},
5065
args: {
5166
video: videoArg,
@@ -68,6 +83,8 @@ export default defineCommand({
6883
const savedTo = save
6984
? recordProjectReverseSession(projectDir, started.session.sessionId, SKILL)
7085
: undefined;
86+
const refused = started.recorded ? refusalWhenServerBlocksFree(started.session) : undefined;
87+
if (refused) throw refused;
7188
return { reverse: summary(started, savedTo), ...probe };
7289
});
7390
},

‎skills-manifest.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"files": 76
2727
},
2828
"reverse-any-video": {
29-
"hash": "025850a14e3b6370",
29+
"hash": "fe326730ec89a06b",
3030
"files": 5
3131
},
3232
"slideshow": {

‎skills/reverse-any-video/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ description: Rebuild any video file as an editable HyperFrames project - motion
99

1010
The result is a HyperFrames project whose render matches the original frame for frame and stays editable. Build to the `/hyperframes` contract and work the way you normally would, with ffmpeg, python and your own eyes. Reverse videos the person has the right to use.
1111

12-
Reverse is a HyperFrames Pro feature: go on only when signed in on a paid plan, not Free. Open with `npx hyperframes reverse start <video> --dir <project>` (a CLI without it: `auth status`, `measure probe`) and `references/start.md`. Resuming (a `REVERSE.md` or half-built project is there) is a start too: open it the same way first. A step an earlier session gave up on (a re-shoot HeyGen refused) gets one more try, then ask the person. If that session ran under a credit limit, just ask whether it still applies, naming no costs or numbers. Rebuild the video as it is; remix it only when the person asks.
12+
Reverse is a HyperFrames Pro feature: go on only on a paid plan, not Free. Open with `npx hyperframes reverse start <video> --dir <project>` (a CLI without it: `auth status`, `measure probe`) and `references/start.md`; if it refuses, pass on its upgrade link and stop. Resuming (a `REVERSE.md` or half-built project) opens the same way. A step an earlier session gave up on (a re-shoot HeyGen refused) gets one more try, then ask the person. If that session ran under a credit limit, just ask whether it still applies, naming no costs or numbers. Rebuild the video as it is; remix it only when the person asks.
1313

1414
## The original decides the frame
1515

‎skills/reverse-any-video/references/start.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ example:
4343
>
4444
> Reply "done" once you're on Pro and I'll start.
4545
46-
Only on a CLI without `reverse start`, give `! npx hyperframes billing subscribe` instead.
46+
When `reverse start` refuses the run (it exits with an error whose `fix` is the upgrade link), give the person that
47+
exact link and stop; once they reply that they are on Pro, run `reverse start` again. Only on a CLI without
48+
`reverse start`, give `! npx hyperframes billing subscribe` instead.
4749
4850
## Signed in on Pro
4951

0 commit comments

Comments
 (0)