ossclip 0.1.28 → 0.1.30
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/editor-dist/assets/index-DfTreNoE.js +166 -0
- package/editor-dist/index.html +1 -1
- package/package.json +4 -4
- package/src/cover.ts +11 -2
- package/src/interactive/produce-argv.ts +14 -0
- package/src/interactive/produce-wizard.ts +28 -0
- package/src/produce.ts +155 -37
- package/editor-dist/assets/index-BBJKC2Z2.js +0 -164
package/editor-dist/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>ossclip editor</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-DfTreNoE.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-Bx2VQLP8.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ossclip",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"description": "Local-first CLI video producer: cuts silence and fillers, word-timed captions, face-aware framing, and LLM-planned code-rendered graphics — transcription and rendering never leave your machine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"commander": "^12.1.0",
|
|
37
37
|
"tsx": "^4.19.0",
|
|
38
38
|
"zod": "^3.25.76",
|
|
39
|
-
"@ossclip/core": "0.1.
|
|
40
|
-
"@ossclip/
|
|
41
|
-
"@ossclip/
|
|
39
|
+
"@ossclip/core": "0.1.30",
|
|
40
|
+
"@ossclip/renderer": "0.1.30",
|
|
41
|
+
"@ossclip/scenes": "0.1.30"
|
|
42
42
|
},
|
|
43
43
|
"homepage": "https://github.com/AhsanAyaz/ossclip#readme",
|
|
44
44
|
"bugs": {
|
package/src/cover.ts
CHANGED
|
@@ -527,12 +527,21 @@ export function coverBannerText(args: {
|
|
|
527
527
|
* theme, which is exactly how a regenerated cover picks up a theme change for
|
|
528
528
|
* free (§corr.2). `production.json` carries neither the RESOLVED theme nor
|
|
529
529
|
* the editor's edits, which is why this is the file that gets read. */
|
|
530
|
-
const CoverRenderPropsSchema = z.object({
|
|
530
|
+
export const CoverRenderPropsSchema = z.object({
|
|
531
531
|
theme: ThemeSchema.optional(),
|
|
532
532
|
settings: z.object({ width: z.number(), height: z.number() }).optional(),
|
|
533
533
|
/** The take's whole-frame subject, for a re-pick's scoring — "screen"
|
|
534
534
|
* zeroes the face weight (scoreCandidate's 2026-08-16 incident). */
|
|
535
|
-
|
|
535
|
+
// `.nullable()`, not just `.optional()` (§154): produce writes `face: null`
|
|
536
|
+
// when the detector found nobody, and null is a MEASUREMENT — core's own
|
|
537
|
+
// schema says so ("null = no face found"). `.optional()` accepts a missing
|
|
538
|
+
// key and rejects an explicit null, so every screen recording and
|
|
539
|
+
// slides-with-voiceover project could not regenerate its cover at all.
|
|
540
|
+
// Downstream reads `renderProps.face?.subject`, which is already null-safe.
|
|
541
|
+
face: z
|
|
542
|
+
.object({ subject: z.enum(["face", "screen"]).optional() })
|
|
543
|
+
.nullable()
|
|
544
|
+
.optional(),
|
|
536
545
|
});
|
|
537
546
|
|
|
538
547
|
export interface CoverRegenerateOptions {
|
|
@@ -63,6 +63,15 @@ export interface ProduceAnswers {
|
|
|
63
63
|
graphics: boolean;
|
|
64
64
|
intent?: string;
|
|
65
65
|
out?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Review the cut in the editor instead of rendering now (§148). A main-flow
|
|
68
|
+
* answer like `graphics`, not an extra: it decides what the run DOES, and
|
|
69
|
+
* burying it in "Anything else?" would hide it from the people the wizard
|
|
70
|
+
* exists for. The only answer whose wizard default (review) differs from
|
|
71
|
+
* the CLI's (render) — which costs nothing here, because the elision rule
|
|
72
|
+
* below keys off the CLI default, not off what the prompt preselected.
|
|
73
|
+
*/
|
|
74
|
+
review?: boolean;
|
|
66
75
|
extras: ProduceExtras;
|
|
67
76
|
}
|
|
68
77
|
|
|
@@ -76,6 +85,11 @@ export function produceArgv(a: ProduceAnswers): string[] {
|
|
|
76
85
|
if (a.aspect !== "9:16") argv.push("--aspect", a.aspect);
|
|
77
86
|
if (a.cleanup !== "standard") argv.push("--cleanup", a.cleanup);
|
|
78
87
|
if (a.out) argv.push("--out", a.out);
|
|
88
|
+
// Rendering is the CLI's default, so only reviewing is worth saying — the
|
|
89
|
+
// rule above is about the DEFAULT, not about which option the prompt
|
|
90
|
+
// preselected. --out still travels either way: nothing renders now, but the
|
|
91
|
+
// editor's Render button replays command.json, and that is where out lands.
|
|
92
|
+
if (a.review === true) argv.push("--review");
|
|
79
93
|
|
|
80
94
|
if (a.graphics) {
|
|
81
95
|
argv.push("--produce");
|
|
@@ -505,6 +505,33 @@ export async function produceWizard(
|
|
|
505
505
|
) as ProduceExtras["llm"];
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
+
// LAST, because it decides what happens after every answer above it — and
|
|
509
|
+
// asked at all because until §148 the wizard could not reach --review, so
|
|
510
|
+
// `ossclip` with no arguments, the entry point a first-time user takes,
|
|
511
|
+
// could only render first and offer the editor afterwards. That is the
|
|
512
|
+
// opposite of what --review is for: the render is the expensive step, and
|
|
513
|
+
// reviewing exists so it happens once, on a cut you already agreed with.
|
|
514
|
+
//
|
|
515
|
+
// Leaning to review is the one place a wizard default differs from the
|
|
516
|
+
// CLI's. It costs nothing: produceArgv elides against the CLI default, so
|
|
517
|
+
// choosing to render still teaches `ossclip produce <file>` and choosing to
|
|
518
|
+
// review teaches the flag that did it.
|
|
519
|
+
const review =
|
|
520
|
+
unwrap(
|
|
521
|
+
await select({
|
|
522
|
+
message: "Render now, or review the cut first?",
|
|
523
|
+
initialValue: "review",
|
|
524
|
+
options: [
|
|
525
|
+
{
|
|
526
|
+
value: "review",
|
|
527
|
+
label: "Review the cut first",
|
|
528
|
+
hint: "opens the editor; render from its button",
|
|
529
|
+
},
|
|
530
|
+
{ value: "render", label: "Render now", hint: "straight to a finished file" },
|
|
531
|
+
],
|
|
532
|
+
}),
|
|
533
|
+
) === "review";
|
|
534
|
+
|
|
508
535
|
return produceArgv({
|
|
509
536
|
input,
|
|
510
537
|
aspect,
|
|
@@ -514,6 +541,7 @@ export async function produceWizard(
|
|
|
514
541
|
// Already `string | undefined`: pickSavePath's use-default row IS the
|
|
515
542
|
// old empty answer — no --out, produce derives its own default.
|
|
516
543
|
out,
|
|
544
|
+
review,
|
|
517
545
|
extras,
|
|
518
546
|
});
|
|
519
547
|
}
|
package/src/produce.ts
CHANGED
|
@@ -152,6 +152,7 @@ import {
|
|
|
152
152
|
type LlmEffort,
|
|
153
153
|
type LlmProvider,
|
|
154
154
|
type LlmUsage,
|
|
155
|
+
AGY_PRINT_TIMEOUT,
|
|
155
156
|
type Production,
|
|
156
157
|
ossclipOutputPathFor,
|
|
157
158
|
type ProviderName,
|
|
@@ -346,6 +347,61 @@ export function beatSheetCacheKey(parts: {
|
|
|
346
347
|
.slice(0, 8);
|
|
347
348
|
}
|
|
348
349
|
|
|
350
|
+
/**
|
|
351
|
+
* The beat-sheet cache keys to TRY, in priority order (§150).
|
|
352
|
+
*
|
|
353
|
+
* §143 split the cache: reads use the provider you asked for, writes file
|
|
354
|
+
* under the one that actually answered. That is right for attribution and
|
|
355
|
+
* wrong for re-runs — while agy keeps timing out, a run that asks for
|
|
356
|
+
* antigravity reads a key nothing will ever write. It re-attempts, waits out
|
|
357
|
+
* the whole print-timeout, falls back, and rewrites the key nobody reads.
|
|
358
|
+
* Every re-render therefore re-plans, the plan differs each time, and editor
|
|
359
|
+
* edits anchored to scenes the new plan no longer has are dropped — a
|
|
360
|
+
* re-render silently rewriting an approved cut (2026-08-23: "edit for
|
|
361
|
+
* scene-11 dropped — the plan no longer has that scene").
|
|
362
|
+
*
|
|
363
|
+
* So the read tries a second key, and exactly one: the provider THIS run
|
|
364
|
+
* would fall back to anyway. Serving that sheet is not a substitution — it is
|
|
365
|
+
* what this run would produce, without paying the timeout to rediscover it.
|
|
366
|
+
* Anything looser (any provider's sheet, newest file wins) would hand a
|
|
367
|
+
* claude-cli plan to someone who asked for gemini and got gemini.
|
|
368
|
+
*/
|
|
369
|
+
/**
|
|
370
|
+
* The producer stamp already on disk, or undefined (§152).
|
|
371
|
+
*
|
|
372
|
+
* Read so a run that answered NOTHING can keep it. `production.json` is
|
|
373
|
+
* rewritten on every produce, cached or not, and the stamp is rebuilt from
|
|
374
|
+
* this run's usage records — which on a cached run are empty, so it fell
|
|
375
|
+
* through to the provider we ASKED for and quietly overwrote a truthful
|
|
376
|
+
* "antigravity → claude-cli" with "antigravity". Attribution belongs to the
|
|
377
|
+
* run that produced the plan, and a cached run produced nothing.
|
|
378
|
+
*
|
|
379
|
+
* Tolerant on purpose: a missing, unreadable or stamp-less file all mean "no
|
|
380
|
+
* prior attribution", which is the same answer a first run gives.
|
|
381
|
+
*/
|
|
382
|
+
export function existingProducerStamp(work: string): Production["producer"] | undefined {
|
|
383
|
+
try {
|
|
384
|
+
const raw = JSON.parse(readFileSync(join(work, "production.json"), "utf8")) as {
|
|
385
|
+
producer?: Production["producer"];
|
|
386
|
+
};
|
|
387
|
+
return raw.producer;
|
|
388
|
+
} catch {
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function beatCacheKeyCandidates(
|
|
394
|
+
parts: Omit<Parameters<typeof beatSheetCacheKey>[0], "providerName">,
|
|
395
|
+
providerName: string,
|
|
396
|
+
fallbackName: string | undefined,
|
|
397
|
+
): string[] {
|
|
398
|
+
const keys = [beatSheetCacheKey({ ...parts, providerName })];
|
|
399
|
+
if (fallbackName && fallbackName !== providerName) {
|
|
400
|
+
keys.push(beatSheetCacheKey({ ...parts, providerName: fallbackName }));
|
|
401
|
+
}
|
|
402
|
+
return keys;
|
|
403
|
+
}
|
|
404
|
+
|
|
349
405
|
/**
|
|
350
406
|
* The clip-window cache key (`clipwindow-<hash>.json`), which answers a
|
|
351
407
|
* different question — WHICH ~Ns of the take to keep (R19 §93) — but asks it
|
|
@@ -1413,6 +1469,14 @@ export function cleanupChoicesLine(vetoed: readonly Segment[], outputDuration: n
|
|
|
1413
1469
|
* and even on a talking head a 7% lurch reads as the camera stumbling. Big
|
|
1414
1470
|
* enough to break up the jump, small enough to pass as sensor noise.
|
|
1415
1471
|
*/
|
|
1472
|
+
/**
|
|
1473
|
+
* How long a silent antigravity call runs before the spinner admits it (§149).
|
|
1474
|
+
* Well clear of the 17-46s a healthy call takes, so a normal run never shows
|
|
1475
|
+
* the notice, and well short of AGY_PRINT_TIMEOUT, so it lands while the wait
|
|
1476
|
+
* still has somewhere to go.
|
|
1477
|
+
*/
|
|
1478
|
+
export const AGY_SLOW_NOTICE_MS = 30_000;
|
|
1479
|
+
|
|
1416
1480
|
export const FACE_PUNCH_SCALE = 1.015;
|
|
1417
1481
|
|
|
1418
1482
|
/**
|
|
@@ -2338,6 +2402,19 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2338
2402
|
// the grounding check — uses the repaired transcript instead, so a
|
|
2339
2403
|
// mishearing can't reach the screen twice in two different spellings.
|
|
2340
2404
|
const providerName = opts.provider ?? defaultProviderName(process.env, binOnPath);
|
|
2405
|
+
// Timeout fallback (2026-08-22, FINDINGS §143): agy hangs persistently on
|
|
2406
|
+
// the real beat-sheet call, and auto-detection picks it whenever the CLI is
|
|
2407
|
+
// on PATH. When the editorial call times out, ONE other provider answers
|
|
2408
|
+
// instead of the run dying, announced out loud: the user must know which
|
|
2409
|
+
// model planned their video.
|
|
2410
|
+
//
|
|
2411
|
+
// Function-scope because the beat CACHE needs it too (§150) — the key a
|
|
2412
|
+
// fallback wrote is the second key a re-run has to try, and computing it
|
|
2413
|
+
// twice would let the two drift into disagreeing about where the plan is.
|
|
2414
|
+
const llmFallbackName =
|
|
2415
|
+
providerName === "antigravity"
|
|
2416
|
+
? fallbackProviderName(providerName, process.env, binOnPath)
|
|
2417
|
+
: undefined;
|
|
2341
2418
|
let provider: LlmProvider | null = null;
|
|
2342
2419
|
// --youtube brings its own provider (field gap, 2026-08-16): the user's
|
|
2343
2420
|
// real command was `--youtube --llm antigravity` WITHOUT --produce, and the
|
|
@@ -2354,16 +2431,6 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2354
2431
|
if (!opts.provider) {
|
|
2355
2432
|
console.log(detectionLine(providerName));
|
|
2356
2433
|
}
|
|
2357
|
-
// Timeout fallback (2026-08-22, FINDINGS §143): agy hangs persistently on
|
|
2358
|
-
// the real beat-sheet call — 10-minute --print-timeout expiries while
|
|
2359
|
-
// claude-cli planned the same video — and auto-detection picks agy
|
|
2360
|
-
// whenever the CLI is on PATH. When the editorial call times out, ONE
|
|
2361
|
-
// other provider answers it instead of the run dying, announced out loud:
|
|
2362
|
-
// the user must know which model planned their video.
|
|
2363
|
-
const llmFallbackName =
|
|
2364
|
-
providerName === "antigravity"
|
|
2365
|
-
? fallbackProviderName(providerName, process.env, binOnPath)
|
|
2366
|
-
: undefined;
|
|
2367
2434
|
provider = createTieredProvider(providerName, {
|
|
2368
2435
|
model: opts.llmModel,
|
|
2369
2436
|
fastModel: opts.llmFastModel ?? cfg.fastModel,
|
|
@@ -2721,11 +2788,22 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2721
2788
|
words: transcript.words.map((w) => w.text),
|
|
2722
2789
|
aspect: landscape ? ("16:9" as const) : ("9:16" as const),
|
|
2723
2790
|
};
|
|
2724
|
-
|
|
2725
|
-
|
|
2791
|
+
// Two keys, tried in order: the provider asked for, then the one this run
|
|
2792
|
+
// would fall back to anyway (§150). Without the second, a workdir whose
|
|
2793
|
+
// plan was written by a fallback can never be read again while the primary
|
|
2794
|
+
// keeps failing — every re-render re-plans, and edits anchored to scenes
|
|
2795
|
+
// the new plan drops go with it.
|
|
2796
|
+
const cacheKeys = beatCacheKeyCandidates(beatKeyParts, providerName, llmFallbackName);
|
|
2797
|
+
const cacheKey = cacheKeys[0]!;
|
|
2798
|
+
const hitKey =
|
|
2799
|
+
cacheKeys.find((k) => existsSync(join(work, `scenes-${k}.json`))) ?? cacheKey;
|
|
2800
|
+
const sceneCache = join(work, `scenes-${hitKey}.json`);
|
|
2726
2801
|
// The cover needs the editorial copy, which is not in the scene list — a
|
|
2727
2802
|
// cached run must still be able to write one.
|
|
2728
|
-
|
|
2803
|
+
// Same key the scenes came from — the hook and cover copy belong to THAT
|
|
2804
|
+
// plan, and pairing a cached scene list with a different sheet's hook
|
|
2805
|
+
// would caption the video with copy for a plan it is not showing.
|
|
2806
|
+
const beatCache = join(work, `beatsheet-${hitKey}.json`);
|
|
2729
2807
|
if (clipFresh) {
|
|
2730
2808
|
// The selection call already planned the scenes (§93d: ONE editorial
|
|
2731
2809
|
// call chooses the window and the beats inside it) — adopt them and
|
|
@@ -2763,7 +2841,15 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2763
2841
|
);
|
|
2764
2842
|
} else if (existsSync(sceneCache)) {
|
|
2765
2843
|
scenes = z.array(SceneSchema).parse(JSON.parse(await readFile(sceneCache, "utf8")));
|
|
2766
|
-
|
|
2844
|
+
// Naming the fallback is the same obligation the live fallback line has
|
|
2845
|
+
// (§143: the user must know which model planned their video). A silent
|
|
2846
|
+
// hit here would let a claude-cli plan read as an antigravity one purely
|
|
2847
|
+
// because it came from disk this time.
|
|
2848
|
+
console.log(
|
|
2849
|
+
hitKey === cacheKey
|
|
2850
|
+
? `▸ scenes cached (${scenes.length})`
|
|
2851
|
+
: `▸ scenes cached (${scenes.length}) — planned by ${llmFallbackName}, which ${providerName} fell back to`,
|
|
2852
|
+
);
|
|
2767
2853
|
if (existsSync(beatCache)) {
|
|
2768
2854
|
// Pre-§118b caches carry no accounting — the report then simply
|
|
2769
2855
|
// omits the graphics section rather than guessing one.
|
|
@@ -2787,17 +2873,38 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2787
2873
|
: null;
|
|
2788
2874
|
if (!aiAnim) console.log(`▸ producing scenes (${providerName})…`);
|
|
2789
2875
|
if (opts.forceComponent) console.log(`▸ forcing every graphic to ${opts.forceComponent}`);
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2876
|
+
// A hung agy is indistinguishable from a working one on screen: the
|
|
2877
|
+
// 2026-08-23 field run sat on this spinner for 605.9s with no hint that
|
|
2878
|
+
// a budget existed or that a recovery was coming, which reads as a
|
|
2879
|
+
// freeze rather than as waiting (§149). Only antigravity has a
|
|
2880
|
+
// print-timeout and a fallback, so only it gets the notice — and only
|
|
2881
|
+
// once the call is actually slow, so a healthy run never sees it.
|
|
2882
|
+
const slowNotice =
|
|
2883
|
+
aiAnim && providerName === "antigravity"
|
|
2884
|
+
? setTimeout(
|
|
2885
|
+
() =>
|
|
2886
|
+
// Short on purpose: StageAnimator clamps the subtitle to the
|
|
2887
|
+
// terminal width and floors that at 40 columns, and the first
|
|
2888
|
+
// draft lost the budget to "falling back to the n...". The
|
|
2889
|
+
// number is the only part that changes what the user does
|
|
2890
|
+
// (wait vs. Ctrl-C), so it has to survive the clamp.
|
|
2891
|
+
aiAnim.update(`agy not replying — falling back at ${AGY_PRINT_TIMEOUT}...`),
|
|
2892
|
+
AGY_SLOW_NOTICE_MS,
|
|
2893
|
+
)
|
|
2894
|
+
: undefined;
|
|
2895
|
+
const result = await phases
|
|
2896
|
+
.time("llm", () =>
|
|
2897
|
+
produceScenes(provider!, {
|
|
2898
|
+
transcript,
|
|
2899
|
+
outputDuration: map.outputDuration,
|
|
2900
|
+
intent: opts.intent,
|
|
2901
|
+
speaker: opts.speaker ?? cfg.speaker,
|
|
2902
|
+
forceComponent: opts.forceComponent,
|
|
2903
|
+
framing: framingCtx,
|
|
2904
|
+
aspect: landscape ? "16:9" : "9:16",
|
|
2905
|
+
}),
|
|
2906
|
+
)
|
|
2907
|
+
.finally(() => clearTimeout(slowNotice));
|
|
2801
2908
|
if (aiAnim) aiAnim.stop();
|
|
2802
2909
|
scenes = result.scenes;
|
|
2803
2910
|
beatSheet = { hook: result.beatSheet.hook, coverText: result.beatSheet.coverText };
|
|
@@ -2873,18 +2980,29 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2873
2980
|
// single-provider runs stamp exactly as before.
|
|
2874
2981
|
const answered = provider.usage.filter((r) => !r.failed);
|
|
2875
2982
|
const providersSeen = [...new Set(answered.map((r) => r.provider))];
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2983
|
+
// A run that answered NOTHING must not restamp the artefact (§152). On a
|
|
2984
|
+
// fully cached run `answered` is empty, `last.provider` is the run we just
|
|
2985
|
+
// appended — whose provider is the one we ASKED for — and the stamp
|
|
2986
|
+
// silently rewrote a truthful "antigravity → claude-cli" into
|
|
2987
|
+
// "antigravity", crediting the plan to a provider that never produced it.
|
|
2988
|
+
// The plan did not change this run, so neither does its attribution.
|
|
2989
|
+
const priorStamp = existingProducerStamp(work);
|
|
2990
|
+
if (answered.length === 0 && priorStamp) {
|
|
2991
|
+
producerStamp = priorStamp;
|
|
2992
|
+
} else {
|
|
2993
|
+
producerStamp = {
|
|
2994
|
+
provider:
|
|
2995
|
+
providersSeen.length > 1
|
|
2996
|
+
? providersSeen.join(" → ")
|
|
2997
|
+
: providersSeen[0] ?? last.provider ?? providerName,
|
|
2998
|
+
// `last.models` already excludes failed attempts' models (usage.ts,
|
|
2999
|
+
// same §143 rule) — a stamp that listed the timed-out placeholder read
|
|
3000
|
+
// "planned by claude-cli (antigravity-default)" after a fallback run.
|
|
3001
|
+
models: last.models,
|
|
3002
|
+
cached: last.cached,
|
|
3003
|
+
at: last.at,
|
|
3004
|
+
};
|
|
3005
|
+
}
|
|
2888
3006
|
}
|
|
2889
3007
|
|
|
2890
3008
|
// The overlay and the caption under it must spell the same word (§21).
|