Changelog
September 23, 2026 · View on GitHub
All notable changes to capcut-cli-david are documented here.
Format follows Keep a Changelog 1.1;
this project adheres to SemVer 2.0.0.
Fork-specific sub-sections (Synced from upstream, Compatibility) are added
per RELEASE.md §4.
Unreleased
Planned
1.x— seerelease-notes/1.0.0.md§Roadmap for the non-binding 1.x backlog.
2.8.0 draft — measured font metrics for cascade-words, voice-calibration package
Added
cascade-wordsresolves a readable font from the catalogue/drafts and measures OpenType layout for line wrapping and word placement.--clone-styleremains a supported fallback when its guide style is readable and linear.fontkitis now the first production runtime dependency, and generated text materials receive a consistent font identity and effective size.
Changed
cascade-wordsuses measured widths and measured prefixes; the effective size priority is explicit--font-size, cloned style,material.font_size, then15.- Font parsing, validation and layout planning happen before draft mutation, so malformed or unreadable font inputs fail without partially writing the draft.
voice-calibrationmoved out of this repo into its own repository atDavidb-2107/voice-calibration(C:\Users\dbele\src\voice-calibrationlocally).capcut-david calibration-uiremains a thin adapter that imports thevoice-calibrationpackage (linked vianpm linkfor development,file:../voice-calibrationinpackage.json). The CLI no longer builds the package; it consumes its builtdist/. The CLI keeps a single bin (capcut-david); the standalonevoice-calibrationbin ships from its own repo.
Removed
--max-charswas removed. Existing invocations must select a font (or use a valid--clone-style) and rely on measured wrapping instead of character counts.- The
packages/voice-calibrationnpm workspace and thenpm run build -w voice-calibrationbuild step (moved to the standalone repository).
Fixed
restylewith a font-bearing preset no longer overwrites the root<draft>/draft_content.json.bakrollback. The font mirror keeps refreshing CapCut's read targets (template-2.tmp,Timelines/<guid>/*), but the draft store's pre-edit bytes (original indent) survive on the root.bak, so the user can always undo a caption-font restyle - as with every other write verb.
Compatibility gate
- The OpenType-to-CapCut scale still requires a real CapCut/cutcli calibration and visual render check; no empirical scale factor is claimed until
docs/cascade-words-font-calibration.mdrecords that result.
2.7.0 — 2026-08-11
Minor release. Lot mémoire : le catalogue apprend les 4 familles CapCut qui lui manquaient, s'ouvre aux ressources sans draft témoin, et se donne à lire en HTML.
Added
catalogue --add <resource_id> --kind <k> --name <nom> [--note <texte>]— entre une ressource dont le draft témoin n'existe plus :--syncne peut rien pour elle puisqu'il n'y a rien à moissonner. Cas réel : les polices Disco / SpeedLines / CC-DerStil, dont les id avaient été relevés dans une session d'enquête sur le dropdown « System » et vivaient depuis dans un fichier de gotchas. Exclusif avec--sync, honore--dry-run, et ne touche que l'entrée visée — délibérément hors deplanCatalogueMerge, qui remet à zéro leswitness_draftsde toutes les entrées non ignorées.--kinds'élargit de 4 à 8 familles —sticker,mask,animation,curve(courbe de keyframe) — dansquerycomme danscatalogue(--syncles moissonne,--addet le filtre de listing les acceptent). Terrains réels : stickers dansmaterials.stickers[], masques dansmaterials.common_mask[](singulier —materials.masksn'existe pas), animations doublement imbriquées (materials.material_animations[].animations[], les slots àid:""ignorés), courbes en tableau racinekeyframe_graph_list[](nom =resource_name, lues même sans blocmaterials). La liste des kinds est factorisée (KINDSdansquery.ts, consommé parcatalogue.ts) : les messages d'usage/erreur s'en dérivent, plus aucune copie en dur dans le code.- Cartographie visuelle du catalogue —
src/ui/catalogue-template.htmlrendu en HTML autonome (même charte que la carte des capacités) et mirroiré dans<vault>/cartographie/capcut-cli-catalogue.html: les 8 familles en sections filtrables, recherche plein-texte (noms, id, notes), badges témoins/sans-id, notes humaines affichées. Régénération best-effort après chaque écriture du catalogue (--sync,--add, au sein dewriteCatalogueAtomic) et ànpm run build; jamais bloquante pour l'écriture JSON.
Fixed
- macOS uniquement, deux bugs invisibles sur Windows/Linux. (a) Le garde
« racine de drafts temporaire → catalogue par défaut » de
catalogue --synccomparait des chemins bruts :mkdtemprend/var/...ettmpdir()/private/var/...(lien symbolique), donc le garde restait muet là-bas et un sync depuis un dossier temporaire écrivait le catalogue par défaut — l'inverse de ce qu'il promet. (b)--helpsortait parconsole.log+process.exit(0); un pipe étant asynchrone sur macOS, l'aide était coupée au tampon et tout ce qui suit la section Add disparaissait dès que la sortie était redirigée (antérieur à 2.6.0).catalogue -Hpasse aussi parprocess.exitCodepour ne pas tronquer un gros catalogue. - CI verte de bout en bout :
biome.jsondéclarait le schéma2.0.0alors que la CLI est en2.4.15, ce qui faisait échouerbiome cisur ce seul décalage et masquait le rouge macOS ci-dessus depuis plusieurs commits. --kind=font(forme=) devenait le terme de recherche dequery: « zéro résultat, exit 0 », une mauvaise réponse silencieuse.--kinda maintenant la même garde de valeur que--name(forme=refusée, valeur obligatoire). Aligné au passage :--kind ""est une erreur d'usage dansquerycomme il l'était déjà danscatalogue, et la signaturecataloguedu registrecapabilities.tsn'est plus en contradiction avec son propre tableau de flags sur la place de--kind(il manquait dans--add).
2.6.0 — 2026-08-07
Minor release. Lot découverte : query --all et catalogue — le CLI passe à 41 verbes,
et il gagne une mémoire qui survit à la suppression des drafts.
Added
query --all— inventaire complet de la bibliothèque de drafts, sans terme de recherche. Répond à « qu'est-ce que le CLI sait nommer aujourd'hui ? » : toutes les polices, effets, filtres et transitions rencontrés dans les drafts, avec leurresource_id. Se combine à--kind(une rubrique à la fois) et-H. Sans--all, un terme reste obligatoire (exit 1) — comportement inchangé.catalogue— mémoire persistante des ressources validées. Fige nom +resource_idde chaque police / effet / filtre / transition vue dans un draft, dans<vault>/Shared/capcut-catalogue.json(résolu par ancre,--cataloguepour outrepasser, dossier courant hors vault). Append-only : une entrée survit à la suppression du draft témoin. Les champsnoteetignoredsont écrits à la main et ne sont jamais réécrits par un sync.--syncmoissonne,--dry-runrapporte sans écrire,--kindfiltre l'affichage. Design :docs/superpowers/specs/2026-08-07-catalogue-design.md.build-uiécrit un miroir de la page capacités danscartographie/du vault quand il tourne depuis celui-ci (racine détectée par ancre) ; sauté ailleurs.
Fixed
- Un BOM UTF-8 en tête de
draft_content.jsonrendait le draft invisible au scan de la bibliothèque :queryle comptait comme illisible et le sautait en silence, donc une police n'existant que dans ce draft était introuvable.
2.5.0 — 2026-07-17
Minor release. Lot catalogue/timeline : add-transition, --full, remove-segment — le CLI passe à 40 verbes.
Added
add-transition <project> <segment-id> <resource-id> <name> [--duration <t>]— attache une transition de catalogue au segment vidéo donné (référencée depuis lesextra_material_refsdu segment AVANT la transition, pas de piste dédiée — forme miroir du témoin CapCut). Défaut--duration0.4s (défaut CapCut). 40ᵉ verbe ; garde CapCut-ouvert appliquée.--fullsuradd-effectetadd-filter— applique sur toute la timeline (start=0, duration=draft.duration) sans passer<start> <duration>; erreur propre si le draft n'a pas de durée.remove-segment <project> <segment-id>— retire proprement un segment : piste supprimée si vidée, puis sweep des materials orphelins en réutilisant la logiquegc(un material encore référencé par un autre segment n'est jamais supprimé). 39ᵉ verbe ; garde CapCut-ouvert appliquée.
2.4.0 — 2026-07-17
Minor release. add-filter — applique un filtre CapCut natif via son resource_id de catalogue.
Added
add-filter <project> <resource-id> <name> <start> <duration> [--value <n>]— applique un filtre (famille Filters, distincte des effets vidéoadd-effect) sur une plage temporelle. Materialtype:"filter"dansmaterials.effects+ pistefilterdédiée (render_index10000,apply_target_type0) — forme miroir de ce que CapCut écrit lui-même quand un filtre est posé dans l'UI. 38ᵉ verbe au registre ; garde CapCut-ouvert appliquée.
Fixed
loadDrafttolère un BOM UTF-8 en tête des JSON de draft (PowerShellSet-Content -Encoding utf8en écrit un).
2.3.0 — 2026-07-14
Minor release. Per-card caption color cycling — alternate a caption's base color across a repeating palette, card by card (the "1 word / alternating color" karaoke look).
Added
import-captions --color-cycle "#hex,#hex,..."— cardi's base text color becomescycle[i % n], overriding the uniform--color/default#FFFFFF. Applies to both the lean and--clone-stylepaths. Verified against a real CapCut draft:restyle(font/stroke/shadow preset swap) preserves the cycled colors unchanged.
Compatibility
import-captionswithout--color-cycleis byte-identical to 2.2.0 — the cycle is an additive, opt-in override.
[2.2.0] — 2026-07-13
Minor release. Capabilities UI — read-only ui verb and a self-contained embedded HTML page cataloguing all 37 verbs (registry, anti-drift tested), with native discovery (no external resources).
Added
uiverb — read-only discovery and inspection.--print-pathoutputs the path to the embedded capabilities HTML page (headless orchestrator gate).- Typed capability registry — all 37 verbs catalogued in
src/capabilities.ts: verb, category, summary, signature, flags, example, read-only/CapCut-closed badges, first release. An anti-drift test asserts a strict 1:1 mapping between registry cards and the verbs actually dispatched bysrc/index.ts. - Embedded HTML page —
dist/ui/index.html, generated at build time byscripts/build-ui.mjs(registry + version injected as inline JSON). Self-contained: zero external resources, works overfile://— asserted by test.
Compatibility
uiis read-only and has no side effects on drafts.
[2.1.0] — 2026-07-13
Minor release. Batch media verbs — init --width/--height, and add-video/add-audio/add-keyframe --batch @file for building a mono-engine montage pipeline without shelling out per-clip.
Added
init --width/--height— canvas override at draft creation, for portrait (9:16) drafts without needingcutclias a first step.add-video --batch @items.json— add many video segments in one call:[{path,start,duration,width?,height?,volume?,trackName?}]. All-or-nothing (any item failing validation aborts before any write), onesaveDraftcall for the whole batch, and returns orderedsegment_ids/material_ids/track_idsplus acount.add-audio --batch @items.json— same contract for audio (SFX/multi-track narration in one save):[{path,start,duration,volume?,trackName?}].add-keyframe --batch @entries.json— Ken Burns / keyframe batches in one save:[{segment_id,property,keyframes:[{time,value,curve?}]}]. ReusescmdAddKeyframeinternally withsave=falseper entry and a singlesaveDraftat the end (the existingif (save)guard oncmdAddKeyframeis what makes this safe — unitaryadd-keyframecalls are unaffected).--helpnow advertises all three--batchforms — orchestrators can grep--helpfor--batchas the ≥ 2.1.0 version-gate marker (same probe pattern as prior releases).
Compatibility
- Unitary (non-
--batch)add-video/add-audio/add-keyframeandinit(no--width/--height) are byte-identical to 2.0.1 — the batch paths are additive, opt-in flags. --batchcannot be combined with a positional file argument (die("--batch cannot be combined with a positional file")).
2.0.1 — 2026-07-12
Patch release. One bugfix, no API changes, no new verbs.
Fixed
tracks -Hcrashed on tracks without aname(Cannot read properties of undefined (reading 'padEnd')).track.nameis optional on disk — tracks created by other tools (cutcli audios add/cutcli videos add) omit it, but theTracktype declared it required, letting the unguardedpadEndpass the compiler.Track.nameis nowname?: stringandcmdTracksnormalizes to""— one guard covers both the-Htable and the JSON output (which previously dropped the key silently). Found on a real Stickman A/B draft carrying a cutcli-created SFX track (repro 2026-07-12,rire-de-tout_AB-A).
Compatibility
- Read-only inspection path; no draft output changes — the byte-identity contract (v1.3.0) is untouched.
2.0.0 — 2026-07-06
Major release. add-audio / add-video write a portable draft-path token instead of an absolute path — media now survives CapCut duplicating/renaming the draft folder (v_slug → v_slug(1)). Fixes the "Link media, couldn't find narration.mp3" dialog on drafts built by capcut-david (repro 2026-07-06, refuser-les-compliments). Aligns the engine with what cutcli already does.
Fixed
- Audio/video unlinked after draft duplication/rename.
addAudio/addVideoused to copy the source into<draft>/assets/<type>/<name>and write an absolutematerial.pathcontaining the draft folder name — dead as soon as CapCut (or the user) renames the folder. They now copy into<draft>/Resources/<matId><ext>and write##_draftpath_placeholder_<UUID>_##\Resources\<matId><ext>, which CapCut re-resolves relative to the draft's own folder. All three consumers are repaired at once (Stickmanassemble_draft.py, Repost_Amélioré phase 5,pipeline.tsself-contained builds). - The placeholder UUID is a per-install constant absent from
draft_meta_info.json/root_meta_info.json, so it is discovered by scanning a token already present indraft_content.json(newdraftPlaceholderTokenhelper;cutcli videos addwrites one before anyadd-audio). Fallback for token-less drafts: the draft's own GUID.
Changed (BREAKING)
material.pathform foradd-audio/add-video:Resources/+ placeholder token instead ofassets/<type>/+ absolute path; the copied file is named<matId><ext>instead of the source basename (material.name/material_namekeeps the human-readable source filename). Byte-identity with 1.x output is intentionally broken — bug fix that changes output = honest major.validate --check-assetsneeds no change: placeholder tokens were already skipped.
Compatibility
- The
--helptext foradd-audio/add-videonow advertises the portable "draftpath token" — orchestrators use this line as the ≥ 2.0.0 version-gate marker (same probe pattern as--transform-y). - Drafts written by 1.x keep their absolute paths and remain openable as long as the draft folder is never renamed; regenerate them with 2.0.0 to make them rename-safe (no data migration — episodes are regenerable).
[1.16.0] — 2026-06-12
Minor release. import-captions --transform-y <n>: vertical caption position is now engine-native — every rebuilt caption segment gets clip.transform.y, retiring the orchestrator-side post-import re-pin (Niche_PC's step 3b). Output is byte-identical when the flag is absent.
Added
import-captions --transform-y <n>— setsclip.transform.yon every rebuilt caption segment (previously hardcoded to{x:0, y:0}bybaseSegment). Global flag only — no per-card field. Works identically on the lean and--clone-stylepaths (the segment build is shared).transform.xis never touched.- Validation via the new
parseFiniteFlag: any finite number is accepted — negatives and zero allowed (e.g.-0.4= mi-bas,-0.6= lower third), unlike the strictly-positive size flags. Non-numeric, empty AND whitespace-only values die with a CliError (theNumber("") === 0coercion trap is guarded explicitly). The two malformed forms that would otherwise fall through to ignored positionals and silently recenter the captions at y=0 are refused too: the equals spelling (--transform-y=-0.4) and a value-less trailing--transform-yboth die instead of exiting 0. --helpadvertises--transform-y— orchestrators can probe the help text as a version gate before relying on the flag (a ≤ 1.15.x engine would silently swallow it as an ignored positional, importing captions recentered at y=0).
Compatibility
- Byte-identity preserved. A new oracle regression test freezes the exact v1.15.0 segment bytes (ids normalized) for the flag-absent path. Test suite: 478 → 494 passing.
restyleafter animport-captions --transform-ygrafts the keys present in the preset'ssegmentblock onto every caption segment — a position-bearing preset (one whosesegment.clipcarries a transform, like the vault gabarits) then wins over the imported y. A preset withoutsegment.clip(e.g. everymake-presetoutput, which emitssegment: {}) leaves the imported position intact. Order your chain accordingly.- One deliberate side effect on other commands:
--transform-yis parsed in the global flag loop, so commands that join positionals into text (add-text,set-text) no longer swallow a literal--transform-y <n>into the rendered text — the pair is now consumed and ignored (and a non-numeric next token dies). On ≤ 1.15.x the same invocation visibly garbled the caption text; on add-text the vertical-position flag remains--y. - No other schema, dependency or behavior change; read commands untouched.
[1.15.0] — 2026-06-12
Minor release. Per-word highlight size: the keyword-highlight system can now enlarge the emphasized word(s), not just recolor them — the native equivalent of Niche_PC's custom apply_keyword_highlight.py size pass. Output is byte-identical when no size option is used.
Added
TextHighlight.size(API) — optional per-span font size in points. Only the emphasis span carries it: base/gap spans keep the basefontSize(lean path) or the cloned style's size (--clone-stylepath) exactly as before.add-text --keyword-size <n>— font size for the highlighted word/range; combines freely with--keyword/--keyword-range/--keyword-color.import-captions --highlight-size <n>— global default highlight size; new per-card JSON fieldhlSizewins over the flag (--keyword-sizeaccepted as an alias, mirroring the--keyword-coloralias). Cards with neither are emitted byte-for-byte as in 1.14.1.- Validation:
--keyword-size/--highlight-sizeflags AND the per-cardhlSizefield must be a finite number > 0 when present (CliError otherwise — a typo'dhlSizeis refused instead of being written verbatim into the draft).
Compatibility
- Byte-identity preserved. Two new oracle regression tests lock the size-less lean and clone span outputs to the exact 1.14.1 byte strings (independently differential-tested against the published 1.14.1 npm dist). Test suite: 464 → 478 passing.
- A pre-existing
hlSizekey in old card JSONs becomes active (it was an unknown, silently-ignored field in ≤ 1.14.1). restylegrafts the preset's style (including itssize) onto every span, preserving onlyfill+range— a per-span size override does not survive a laterrestyle; re-apply sizes after restyling.
[1.14.1] — 2026-06-09
Patch release. Bugfix for sync-timelines: it now reconciles the guid's draft_info.json — the mirror CapCut reads on the first open of a CLI-built draft — so audio and captions added after the build are no longer silently dropped.
Fixed
sync-timelinesnow reconcilesTimelines/<guid>/draft_info.json. A draft built in the CLI and never opened in CapCut keeps its timeline mirror in the guid'sdraft_info.json; CapCut only materialisesTimelines/<guid>/draft_content.jsonon first open. BecausesaveDraftwrites only the rootdraft_content.json, audio/captions added post-build never reached the file CapCut reads first → they were lost on open (CapCut then rewrote the root video-only on close).draft_info.jsonwas an oversight inMIRROR_FILES, not a deliberate exclusion (only.bak+ the patch journalsmini_draft.json/patch.jsonare excluded). Root cause confirmed by filesystem repro.- Scope = the guid's
draft_info.jsononly.MIRROR_FILESfeeds the per-guid loop, so the rootdraft_info.json(an inert legacy sibling — CapCut does not read it as authority) is never touched. The root-siblings pass (roottemplate-2.tmponly) is unchanged. - Data-loss transparency extended: the stderr
WARNINGfor a divergent overwrite now also fires when the guid'sdraft_info.jsonis overwritten (previously onlydraft_content.json). - Unchanged safety net: timestamped
.synced-<epoch>.bakbefore overwrite, skip-if-absent, skip-when-byte-identical, per-guid isolation, root draft read-only.
- Scope = the guid's
- 464 tests (+6). Typecheck clean; changed files lint-clean.
[1.14.0] — 2026-06-07
Minor release. New read-only make-preset — the generation cousin of query. Given a font name, it scans the CapCut drafts library and emits a ready-to-use bare-font restyle preset (font identity only), so you never hand-craft a preset JSON again.
Added
make-preset --font <name|resource_id> [--out <file>] [--drafts <dir>] [--human]— read-only preset generator. Scans every draft under the projects root (defaultdefaultProjectsRoot(), override--drafts), finds the font, and emits a preset that feeds straight intorestyle --preset.- Match:
--fontis a case-insensitive substring on the catalogue title; if the value is purely numeric it is matched as an exactresource_idinstead. - Source = drafts library (zero new deps). The complete font block (
font_path,font_resource_id,fonts[]incl.title,source_platform) already lives in any draft that uses the font — no SQLite / CapCut-cache reading. (A catalogue font is, by definition, one you've applied at least once, which writes it into a draft.) - Dedupe + tie-break: by
resource_id; on collision prefers the catalogue-grade entry (source_platform == 1, non-emptytitle,.ttfunder/effect/<rid>/) over a bare local fallback, so the same font seen as both local and catalogue collapses to the catalogue truth. - Bare-font preset: carries ONLY the font identity —
text_material.{font_path,font_resource_id,font_title,font_source_platform,fonts[]}+content_template.styles[0].font {path,id}+segment {}. No stroke/shadow/size decoration. Thefonts[]entry is copied verbatim from the draft (path normalized,request_idcleared) so the CapCut dropdown resolves the name (the S1649 #8 correctness detail). --out <file>writes the bare preset (ready forrestyle --preset); without it the preset rides in the JSON envelope.-H/--humanprints a one-line summary / candidate list.- Envelope:
capcut-david/make-preset@1({type, ok, font, ambiguous, candidates[], written, preset}). - Exit codes:
0for success, no-match (ok:false), and ambiguous (lists candidates, no silent pick);2if the drafts root is missing, every draft is unreadable, or the matched font is local-only (noresource_id→ can't build a catalogue preset);1for usage errors (missing--font).
- Match:
- 458 tests (+25). Typecheck clean;
make-preset.tslint-clean.
[1.13.0] — 2026-06-06
Minor release. New read-only query — a catalogue lookup that searches the CapCut drafts library for effects, filters, transitions and fonts by name and returns their resource_id. No writes; complements the validate→fixer family.
Added
query <term> [--kind effect|filter|transition|font] [--drafts <dir>]— read-only catalogue search. Scans every draft under the projects root (defaultdefaultProjectsRoot(), override--drafts), indexes 4 kinds and matches<term>as a case-insensitive substring on the item name, returning each item'sresource_id/effect_id.- Kinds: effects (
materials.effectswheretype != "filter"+materials.video_effects), filters (materials.effectswheretype == "filter"), transitions (materials.transitions), fonts (materials.texts[].fonts[].title, orderiveFontName(font_path)for local.ttfwith no resource_id →resource_id: null). - Dedupe: by
resource_id(local fonts byname+font_path); each result listsfrom_drafts[]. - Envelope:
capcut-david/query@1(flat{type, results[]}, nonext).-H/--humanrenders a table. - Exit codes:
0for any valid query incl. zero matches and an empty library;2if the drafts root is missing or every draft is unreadable;1for usage errors (missing<term>, invalid--kind).
- Kinds: effects (
- 433 tests (+34). Typecheck clean;
query.tslint-clean.
[1.12.0] — 2026-06-06
Minor release. New validate --fix — the umbrella auto-fixer that completes the validate→fixer family: it maps each fixable finding to its command-backed fixer and runs them in dependency order. Dry-run by default; --apply required to write.
Added
validate <project> --fix [--apply]— aggregate auto-fixer. Runsvalidate, maps each fixable finding to its fixer, and either previews the plan (default, zero writes) or applies the fixers in dependency order, then re-validates and reports the residual.- Finding → fixer mapping:
materials.orphan_text/materials.orphan_media→gc(destructive),meta.missing→init-meta,meta.unregistered→register,timelines.divergence→sync-timelines. - Fixed run order
gc → init-meta → register → sync-timelines. Forced by two dependencies:registerneeds the sidecarinit-metawrites (so init-meta runs first), andsync-timelinescopies the rootdraft_content.jsoninto theTimelines/mirror — so it must run strictly last, aftergchas mutated and saved the root. - Dry-run by default (D1).
--fixalone previews the aggregated plan and writes zero bytes;--applyis required to mutate. This intentionally invertsgc/sync-timelines(apply-by-default), because the umbrella aggregates a destructivegcremoval across many findings in one pass.--fix --apply --dry-runis rejected (mutually exclusive). - Reuses
--id/--skip(D3) for selective fixing — a fixer runs iff at least one of its owning finding-ids survives the filter (gcowns two, so skipping one alone doesn't drop it). Re-validates from fresh disk state after applying (D4) and exits on the residual.duration.under/overrunare reported but excluded (fixable but no dedicated command, D2). - Blocking-error refusal. On a
dangling_ref/duplicate_iddraft,--fix --applyrefuses the whole run (exit 2, zero writes) —gc's id-filter is only sound once duplicates are excluded. Dry-run still shows the plan, flaggedblocked. - Conditional CapCut guard.
validatestays OUT ofWRITE_COMMANDS(read-onlyvalidateand dry-run--fixrun with CapCut open);assertCapCutClosedfires only inside the--applywrite path.--force/CAPCUT_DAVID_FORCEbypass. - Exit codes: dry-run → always
0;--apply→reportExitCode(residual)(0/2); blocking-error refusal →2;sync-timelinesfailure or any tool error →1. Note: orphan findings areinfo-severity, so the exit code is not the success signal forgc—fix.results[].wroteis. - Envelope extends
capcut-david/validate@1with an additivefixkey (present only with--fix); a plainvalidateenvelope is unchanged. Nonextfield.
- Finding → fixer mapping:
Changed
init-meta: the sidecar write is extracted into an exportedapplyInitMeta(plan, metaPath)(the bak-first branch + the compactwriteFileSync) sovalidate --fixcan write the sidecar without re-parsing or re-emitting an envelope.cmdInitMeta's behavior and full envelope are byte-identical (regression-locked).
Notes
- No existing command changes behavior (the
applyInitMetaextraction is byte-identical —init-meta's tests stay green).validate --fixadds 24 tests (399 total). Typecheck clean;validate-fix.tslint-clean.
Compatibility
- CapCut ≥ 5.x desktop (Windows + macOS) — unchanged. Node
>= 18— unchanged. Runtime dependencies: zero — unchanged.
[1.11.0] — 2026-06-05
Minor release. New init-meta command — generates the missing draft_meta_info.json sidecar that v1.8.0 validate's meta.missing detects. Completes the validate→fixer family (timelines.divergence→sync-timelines, orphan_*→gc, meta.missing→init-meta).
Added
init-meta <project>— generates the canonicaldraft_meta_info.jsonnext to a draft'sdraft_content.jsonwhen it is missing. Without that sidecar a draft is invisible in the CapCut UI andregisterfails.- Refuse-to-clobber (the INVERSE posture of
sync-timelines/gc). An existingdraft_meta_info.jsonis presumed authoritative (it carries the realdraft_id,tm_draft_*timestamps and cloud refs), andmeta.missingonly fires when the file is absent — soinit-metarefuses (exit 1, zero bytes, sidecar untouched) when one already exists, unless--force(which writes a.bakof the original first and warns on stderr). - Writes ONLY
draft_meta_info.json. Neverdraft_info.json(a stale one could shadowdraft_content.jsoninfindDraft), neverroot_meta_info.json— except the opt-in--registersubstep, which chainsregisterand is explicitly CapCut-open-guarded (init-metaitself is not inWRITE_COMMANDS).draft_content.jsonis never touched. - Field sourcing matches
register's disk-identity rules:draft_idfrom the draft'sid(or a fresh UUID + an stderr note thatregisterwill reconcile),draft_name= the dir basename,draft_root_path= the parent of the draft dir,tm_*in microseconds. The canonical ~40-field shape is built bybuildDraftMetaInfo, extracted verbatim frompsycho-buildintosrc/utils/draft-meta.tsand shared, so both emit a byte-identical sidecar (locked by an exact key-set/order test). - Flags:
--force,--register,--projects-root(forwarded only toregister),--dry-run(writes nothing),-q,-H. Exit codes:0= created (or dry-run / register OK),1= tool failure (not found, resolved file ≠draft_content.json, sidecar already present without--force, write/register failure). No exit 2.
- Refuse-to-clobber (the INVERSE posture of
validate'smeta.missingfix_hintnow namescapcut-david init-meta <project>.
Notes
- No existing command changes behavior. The
buildDraftMetaInfoextraction is byte-identical (psycho-build's 45 tests stay green).init-metaadds 13 tests (375 total);init-meta.jscoverage is 97.8% lines / 100% functions.
Compatibility
- CapCut ≥ 5.x desktop (Windows + macOS) — unchanged. Node
>= 18— unchanged. Runtime dependencies: zero — unchanged.
[1.10.0] — 2026-06-05
Minor release. New gc command — the WRITE verb that removes the orphan materials v1.8.0's read-only validate reports (materials.orphan_text / materials.orphan_media). Completes the validate→fixer trio alongside v1.9.0 sync-timelines.
Added
gc <project>— deletes the segment-orphan text/video/audio materials thatvalidateflags as info (e.g. the leftover text materialsimport-captions/restyleleave behind on every re-injection), by mutatingdraft.materialsin place and re-serialising viasaveDraft.- Scope is the data-safety guarantee. gc removes only from
materials.texts/materials.videos/materials.audios— never any other slot. For those three slots, segment-reachability is total reachability: a cross-material id scan over all 9 fixtures (1283 materials) found zero material→material references, so a segment-orphan there has no inbound edge. It is an orphan media/text GC, not a full GC (it leaves the now-doubly-orphan companion bundle of a deleted text — benign, and validate ignores those by design). - JSON-only. gc never deletes a file under
Resources/(a mediapathmay be shared by another live material) and writes no file butdraft_content.json(+ its.bakviasaveDraft). - Refuses on a broken draft. If
validatewould report an error (materials.dangling_reformaterials.duplicate_id), gc exits 1 and writes nothing — a dangling ref means the draft is already inconsistent; a duplicate id makes "the orphan with id X" ambiguous. - Defensive cross-ref skip. A text whose
text_to_audio_idsis non-empty (the one documented field that can name other-material ids; empty in every fixture) is skipped (reported underskipped_cross_ref), never deleted. - No-op never writes. When nothing is orphaned, gc calls no
saveDraft— the.bakand mtime are untouched (wrote:false), so it is idempotent and the previous edit's rollback is preserved. (After a real removal gc cannot promise byte-identity — the deletions are re-serialised — so the byte-identity guarantee is the no-op path only.) - Apply-by-default +
--dry-run; inWRITE_COMMANDS(the "CapCut is open" guard refuses while CapCut runs,--forcebypass); a stderr WARNING on real removal names the.bakand reminds you to runsync-timelinesafterwards (the root now diverges from anyTimelines/mirrors). Exit codes:0= success incl. nothing-to-gc,1= tool failure. No exit 2.
- Scope is the data-safety guarantee. gc removes only from
validatenow exportscollectOrphans(the shared per-slot orphan definition gc deletes against) andhasBlockingErrors; itsmaterials.orphan_text/materials.orphan_mediafindings'fix_hintnow namescapcut-david gc <project>.
Notes
- No existing command changes behavior.
gcadds 24 tests (363 total);gc.jscoverage is 100% lines / 100% functions.
Compatibility
- CapCut ≥ 5.x desktop (Windows + macOS) — unchanged. Node
>= 18— unchanged. Runtime dependencies: zero — unchanged.
[1.9.0] — 2026-06-05
Minor release. New sync-timelines command — the WRITE verb that repairs the stale-timeline-mirror corruption that v1.8.0's read-only validate detects (timelines.divergence). Once CapCut opens a draft it treats <draft>/Timelines/<guid>/draft_content.json as its own timeline source of truth; a later CLI re-patch of the root leaves that mirror stale and CapCut renders the old state (fonts/captions missing). This automates the documented manual recovery.
Added
sync-timelines <project>— copies the rootdraft_content.json(RAW bytes, never re-serialised) into everyTimelines/<guid>/draft_content.json+template-2.tmp, so root and mirrors agree again. Direction is always root → mirror, never inferred from mtime. It is the fixer forvalidate'stimelines.divergencefinding.- Data-loss safety (apply-by-default + nets): in
WRITE_COMMANDS→ the "CapCut is open" guard refuses while CapCut runs (--forcebypass); timestamped, non-clobbering per-file backups (…synced-<epoch>.bak) before every overwrite; skip-when-byte-identical (a converged draft writes zero bytes, mtime unchanged); a stderr WARNING on each divergent-mirror overwrite reminding you to confirm CapCut has been closed since the draft was modified. The dangerous "mirror is newer than root" case (edited in CapCut, closed, then synced) is recoverable from the timestamped backup. - Never touched: the root
draft_content.jsonitself (read-only source), the rootdraft_content.json.bak(saveDraft's own rollback), the patch journals (attachment/patch/mini_draft.json/patch.json),draft_meta_info.json/root_meta_info.json,key_value.json,Resources/. - Robustness: per-guid isolation — a locked/unwritable mirror degrades to an error (exit 1) without aborting the other guids, and a backup created for a write that then fails is cleaned up (no orphan). No
Timelines/folder ⇒ no-op success (the dir is never fabricated). - Flags:
--dry-run(report without writing a byte),-q,-H,--force. Exit codes:0= synced or nothing to sync,1= tool failure. No exit 2.
- Data-loss safety (apply-by-default + nets): in
- Shared
listTimelineDirs()util (src/utils/timelines.ts) — single source of theTimelines/<guid>walk, now used bymirror.ts,validate.tsandsync-timelines.ts.validate.tsexportsdraftSignature; itstimelines.divergencefix_hintnow namessync-timelines.
Notes
- No existing command changes behavior.
sync-timelinesadds 31 tests (350 total);sync-timelines.jscoverage is 98.6% lines / 100% functions. Themirror.ts/validate.tsrefactor to the shared walk is behaviour-preserving (all prior tests green).
Compatibility
- CapCut ≥ 5.x desktop (Windows + macOS) — unchanged. Node
>= 18— unchanged. Runtime dependencies: zero — unchanged.
[1.8.0] — 2026-06-05
Minor release. New validate command — a strictly read-only linter that detects draft corruption and broken invariants before you open CapCut. It is the diagnostic backbone the future fix verbs (--fix, gc, sync-timelines, relink) will graft onto: every detector is a pure, exported CHECKS function emitting a stable finding schema (id / severity / fixable / fix_hint).
Added
validate <project>— runs a battery of checks overdraft_content.jsonand prints a versioned JSON report (schema: "capcut-david/validate@1") or, with-H, a human table grouped by severity. Never writes a byte (nosaveDraft, no.bak, nomkdir— locked by a byte-identity test) and is deliberately not inWRITE_COMMANDS, so it runs fine while CapCut is open. Each detector is wrapped so a throw degrades into a diagnostic rather than crashing the run; an unparseable draft is a single clean{"error":…}(exit 1).- Exit codes:
0= ran clean (or only info/warnings without--strict),2=errorfindings present (or warnings under--strict),1= tool failure (bad args / project not found / unreadable JSON). So finding problems never collapses to a tool error. - Checks (always-on, CI-safe — pure graph):
materials.dangling_ref(error; resolvessegment.material_idagainst an exact id Set, never the prefix-matchingfindMaterialGlobal),materials.duplicate_id(error; global across all ~54 material slots),companions.missing(warning; each presentextra_material_refsentry must resolve — the slot bundle is polymorphic/positional),segments.zero_duration(error on video/audio/text tracks, warning elsewhere),duration.underrun(warning) /duration.overrun(info) with a µs epsilon,segments.overlap(warning; video/audio only — text/effect/overlay stack legitimately),materials.orphan_text/materials.orphan_media(info; scoped to the typed slots so per-segment companions never cry wolf — and the leftoversimport-captions/restylecreate by design stay info),canvas.config_sanity(warning;fpsis read at the draft root). - Checks (filesystem):
meta.missing(error),meta.unregistered(warning),meta.duplicate_draft_id(warning) run automatically when a directory is passed (a baredraft_content.jsonfile → these report skipped, never error); they readroot_meta_info.jsonfrom--projects-rootordefaultProjectsRoot()and degrade quietly if it is absent.assets.missing_file(--check-assets) andtimelines.divergence(--check-timelines, a cheap duration/segment-count signal — never a deep-equal) are opt-in. - Flags:
-H/--human,-q/--quiet(exit code only — CI gate),--strict,--id <id>/--skip <id>(repeatable),--check-assets,--check-timelines,--projects-root <dir>.
- Exit codes:
Notes
effect.bind_danglingis intentionally not in the MVP: video FX bind viamaterial_idon aneffecttrack while a filter material'sbind_segment_idis legitimately empty (""= track-wide); shipping it without handling both routing modes would cry wolf. It returns to a later minor once the fix verbs need it.- No existing command changes behavior.
validateadds 52 tests (331 total);validate.jscoverage is 97.6% lines / 100% functions.
Compatibility
- CapCut ≥ 5.x desktop (Windows + macOS) — unchanged. Node
>= 18— unchanged. Runtime dependencies: zero — unchanged.
[1.7.0] — 2026-06-05
Minor release. Hardens the engine against the two most expensive CLI footguns documented in the CapCut skills: absolute source paths breaking on Windows, and writing a draft while CapCut is open (silent loss to CapCut's on-close overwrite).
Fixed
- Absolute source paths on Windows.
add-audio/add-videoresolved paths withpath.startsWith("/"), which on Windows misclassifies an absolute path (C:\…/C:/…) as relative and joins it onto the cwd →ENOENT(or the asset silently not copied intoassets/). Now usespath.resolve()(correct on Windows + POSIX) for the source, andpath.basename()(wassplit("/").pop(), which returns the whole string for a backslash path) for the asset filename. A relative input that already worked produces a byte-identical draft (locked by test); an absolute input that previously failed now copies the source into<draft>/assets/<type>/and references it there.
Added
- "CapCut is open" preflight guard. Every draft-writing command (
add-*,set-text,shift/shift-all,speed,volume,trim,opacity,import-captions,restyle,add-keyframe,ken-burns,apply-template,batch,cut,psycho-build,register) now refuses to run while CapCut is detected running, with a clear error (CapCut is running — close it before writing (or pass --force)). CapCut holdsdraft_content.json/root_meta_info.jsonin memory and rewrites them on close, silently discarding CLI edits — this is gotcha #1 in every CapCut skill and Iron Law #4 of the build pipeline. Detection:tasklist(Windows) /pgrep(macOS/Linux); fail-open — if the probe is absent or finds nothing (e.g. CI), the command proceeds. Read-only commands andinit(creates a brand-new draft) are not guarded. --forceflag (andCAPCUT_DAVID_FORCEenv var) to bypass the guard for automation.
Notes
- The guard runs one process-list probe per write command (~tens of ms); library functions (
addText,importCaptions,applyCaptionStyle, …) are never touched, so programmatic/batched use is unaffected. add-text/import-captions/restyleoutput is unchanged — no new behavior on existing paths beyond the absolute-path fix.
Compatibility
- CapCut ≥ 5.x desktop (Windows + macOS) — unchanged. Node
>= 18— unchanged. Runtime dependencies: zero — unchanged. - 279 tracked tests pass (265 prior + 3 absolute-path + 11 guard). The absolute-path fix's Windows regression value is host-OS-specific (POSIX absolute paths already started with
/); it was watched fail→pass on Windows.
[1.6.0] — 2026-06-05
Minor release. New restyle command ports the CapCut-CaptionStyling font-mirroring patchers (restyle.py + fix_fonts_full.py + fix_content_styles_font.py + fix_key_value.py) into the engine, so that skill becomes pure orchestration. It applies a caption style preset (font / stroke / shadow / size) to every caption, span-aware: multi-span keyword-highlight captions keep each span's color + range while the font / stroke / shadow change on all spans (the inverse of --clone-style).
Added
restyle <project> --preset <preset.json> [--track-name <name>]— apply a caption style preset to every caption on the target text track(s) (default: all text tracks;--track-namescopes to one). Reuses thepreset_captions_style.jsonschema (text_material+content_template+segment). Each span keeps itsfill(keyword color) +range;font/strokes/shadows/size/boldcome from the preset. Material-level font fields are grafted (font_path,font_resource_id,fonts[]with emptyrequest_id,has_shadow,shadow_*,border_*);recognize_task_idis cleared (else CapCut Auto-Captions regenerate and wipe the style);base_content/recognize_textare preserved (not blanked from the preset).- Sidecar font-mirroring — writes the new draft to
template-2.tmp+draft_content.json.bak, forcescontent.styles[].font.{path,id}insideTimelines/*/mini_draft.json+patch.json, and injects thekey_value.jsonregistry entry keyed byresource_id. All targets are skip-if-absent;key_value.jsonis never fabricated.
Changed
registernow stamps a freshtm_draft_modifiedon the add path, so a cloned or engine-generated draft surfaces at the top of CapCut's grid instead of inheriting a stale timestamp that buries it at the bottom (present in the index but effectively invisible).
Notes
- Single-span lean captions (legacy
add-textUTF-16 byte-length range) are promoted to code-unit ranges on restyle, matching CapCut's rich-text convention. - Restyle scopes to materials reachable from text-track segments, so orphaned text materials left by
import-captionsare never touched. restyleis a new, separate code path —add-text/import-captionsoutput stays byte-identical (locked by test). The font dropdown showing "System" for a correctly-rendered preset font is a pre-existing CapCut cosmetic quirk, not a regression.
Compatibility
- CapCut ≥ 5.x desktop (Windows + macOS) — unchanged. Node
>= 18— unchanged. Runtime dependencies: zero — unchanged. - 265 tracked tests pass (241 prior + restyle / mirror / register). Restyle output validated against a live CapCut render (CC-DerStil preset on a multi-span keyword caption — per-word colors survive the font change).
[1.5.0] — 2026-06-04
Minor release. import-captions --clone-style preserves the existing caption look (font / strokes / shadows / size) from the target track when injecting keyword-highlight captions — font-agnostic, so any font the user set in CapCut (even one not in any preset library) survives, with the highlight color laid on top. This is the last piece needed to fully replace the standalone inject_word_captions.py patcher (which cloned the draft's caption style via template copy).
Added
import-captions … --clone-style— before replacing the target track, photocopy the style block (font,strokes,shadows,size,bold, …) of that track's first existing caption and apply it to every new caption, overriding onlyrange+fill(solid highlight color). The template material is deep-cloned so material-level fields (text_color,border_*,shadow_*,line_spacing, …) are preserved too. In clone mode every card is rich-text (parity with the patcher). Opt-in — default behavior is unchanged from 1.4.0.buildRichTextContent(text, fontSize, baseColor, highlights, baseStyle?)— optionalbaseStyleparam; when provided each span is a deep copy of it withrange+ solidfilloverridden. Without it, the lean default span shape is byte-for-byte unchanged (locked by test).
Changed
import-captionswithout--track-namenow targets the first existing text track (was: a track literally namedtext). CapCut caption tracks are usually unnamed, so the old default created a stray second track and clone had nothing to read.add-textis unaffected;add-text/import-captionscontent output is otherwise identical to 1.4.0.
Notes
--clone-styleclones from the target track (--track-nameif given, else the first text track). If that track is empty / its content can't be parsed, it falls back to the default style with a stderr warning — never an error.
Compatibility
- CapCut ≥ 5.x desktop (Windows + macOS) — unchanged. Node
>= 18— unchanged. Runtime dependencies: zero — unchanged. - 241 tracked tests pass (235 prior + 6 clone-style). Clone output validated against a live CapCut render with a deliberately non-preset font.
[1.4.0] — 2026-06-04
Minor release. Makes keyword highlight (per-word caption color) a first-class engine feature, replacing the standalone Python inject_word_captions.py patcher. CapCut multi-span rich-text is now produced natively, with colors emitted as float32 (Math.fround) to match CapCut's internal encoding and range offsets in UTF-16 code units.
Added
add-text … --keyword <word> | --keyword-range <s,e> [--keyword-color <hex>]— color one keyword inside a caption.--keywordmatches the first substring occurrence (UTF-16 code-unit offsets via nativeString.indexOf/.length);--keyword-rangetakes explicitstart,endoffsets and takes precedence. Default color#FFD600. A keyword that is not found (or an out-of-bounds range) is a hard error — never a silent no-op.import-captions <project> <captions.json> [--highlight-color <hex>] [--track-name <name>]— batch word/keyword captions from[{text,start,end,hl?:[s,e],color?}]. Replaces the named text track's segments 1:1 with the patcher, leaves prior text materials orphaned (unreferenced ⇒ invisible ⇒ zero deletion risk), supports per-cardcolor(falls back to--highlight-color), and extendsdraft.durationto the last caption end.buildRichTextContent()— shared core that emits N contiguous, non-overlapping CapCut style spans covering[0, text.length]in UTF-16 code units (gaps = base color, ranges = highlight color),is_rich_text: trueon multi-span materials.
Changed
add-text/import-captionssharebuildTextMaterial(). The single-spanbuildTextContentpath is unchanged —add-textwithout a keyword flag is byte-identical to v1.3.0 (single span, UTF-16 byte range, nois_rich_text). Locked by test.
Fixed
set-textandapply-templatenow refuse to mutate a multi-span (keyword-highlight) caption instead of silently corrupting it (they only re-rangedstyles[0], desyncing the remaining keyword spans). Clear error directs the user to rebuild viaadd-text --keyword/import-captions.test-fixtures/fixtures/animations-draft.jsonkeyword-highlight oracle: restored the real caption text"THE EYES ARE WATCHING ME."(the LOREM anonymizer had replaced it at approximate length while leaving the style ranges, desyncing range↔text). Documented the policy exception in the fixtures README.
Compatibility
- CapCut ≥ 5.x desktop (Windows + macOS) — unchanged. Node
>= 18— unchanged. Runtime dependencies: zero — unchanged. - 234 tracked tests pass (208 prior + 26 keyword-highlight). Highlight output validated against a live CapCut render.
[1.3.0] — 2026-05-20
Minor release. Extends the CapCut "Cubic Out" Δ-scaling fix (shipped in v1.2.0 for cmdKenBurns) to the incremental path cmdAddKeyframe. Both code paths now converge on a single shared segment model (computeSegmentHandles).
Fixed
cmdAddKeyframeease-out wrotestart.right_control.y = -0.47fixed regardless of Δvalue at the inserted kf — nowround(0.94 × Δvalue, 6), coherent with the CapCut "Cubic Out" preset and thecmdKenBurnsfix shipped in v1.2.0.cmdAddKeyframedid not retro-compute neighbor handles when inserting between two existing kfs —prev.right_controlandnext.left_controlstayed pinned to obsolete intervals. Now both neighbors are retro-updated:prev.right(x AND y),next.left(x — y stays 0 for all supported curves).
Changed
cmdAddKeyframeoutput:- sequence (
add t=0, val=A, ease-out+add t=T, val=B, ease-out) now produces byte-identical output tocmdKenBurns(A, B, ease-out)on thescale_xcontainer; - insertion between 2 existing kfs now modifies the neighbors as well (cf. Fixed);
- solitary kf (neither prev nor next) — both
left_controlANDright_controlare{x: 0, y: 0}(previouslyrightcarried a "phantom" handle without a destination:{round(0.32 × segDuration), -0.47}); - replace of a kf at the same
timeOffsetnow retro-updates neighbors using the newvalue(so the new Δ propagates).
- sequence (
- The curve specified to
cmdAddKeyframenow applies to both adjacent segments (prev→newandnew→next) — i.e., it overwrites the handles ofprev.rightandnext.leftat insertion. Semantics aligned with CapCut UI where changing a kf's curve affects both surrounding segments. CURVE_PROFILES["ease-out"].startRightY:-0.47→0(internal cleanup; this field is no longer consumed — the real value comes from thecurve === "ease-out"gate viaKEN_BURNS_CUBIC_OUT_RIGHT_Y_RATIO).
Compatibility
- CapCut ≥ 5.x desktop (Windows + macOS), JianYing 6+ unsupported — unchanged.
- Node
>= 18— unchanged. - Runtime dependencies: zero — unchanged.
- New tests targeting
cmdAddKeyframeoracle parity, behavioral coverage, and captured-CapCut-UI byte-identity (208 tracked tests). All tracked tests pass. The new byte-identity oracle (test-fixtures/oracles/cubic-out-triplet-frame-aligned.json) locks a formal contract vs CapCut UI: x byte-identical on frame-aligned intervals, ±1μs on non-aligned, ±1e-9 (~1 ULP) on y. draft_content.jsonoutput foradd-keyframe --curve ease-outis not byte-identical to 1.2.0 whenΔ ≠ -0.5(intended correctness fix).ken-burnsoutput is byte-identical to 1.2.0.
1.2.0 — 2026-05-19
Minor release. Corrects a latent easing bug in ken-burns: the start keyframe's outgoing Bezier handle y was a fixed absolute (-0.47) regardless of the zoom range, so easing was wrong for every range except exactly to − from = -0.5. Not byte-identical for Δ ≠ -0.5 (hence minor, not patch) — see Changed.
Fixed
ken-burnsease-out easing —cmdKenBurnswrotestart.right_control.y = -0.47(theCURVE_PROFILES["ease-out"]fixed value) for every zoom, ignoring the value delta. CapCut's "Cubic Out" preset encodesstart.right_control.y = round(0.94 × (toVal − fromVal), 6); the-0.47was only theΔ = -0.5special case (a1.5 → 1.0zoom). A zoom-in1.0 → 1.12(Δ = +0.12) was getting-0.47instead of0.1128— a grossly wrong curve. Fix:cmdKenBurnsnow derivesyfrom0.94 × Δforease-out(other curves keep their profiley, all0). Validated against the real-capture ground truth (CapCut-ZoomFX/Tools/tests/fixtures/cubic-out-groundtruth.json) and the cutcli-fix parity model. The incrementaladd-keyframe/computeControlPointspath is intentionally not changed (no proven ground truth there) — see release note backlog.
Changed
ken-burnsdraft output:start.right_control.ynow varies with the zoom delta (round(0.94 × (to − from), 6)). Output is byte-identical to 1.1.1 only whento − from = -0.5(e.g.1.5 → 1.0); for any other range the value changes (from the previously-wrong-0.47to the correct Δ-scaled value).start.right_control.x,end.left_control, thexratios, and all non-ease-outcurves are unchanged.cmdAddKeyframeoutput is byte-identical.
Compatibility
- CapCut ≥ 5.x desktop (Windows + macOS), JianYing 6+ unsupported — unchanged.
- Node
>= 18— unchanged. - Runtime dependencies: zero — unchanged.
- 3 new tests (199 tracked tests). All tracked tests pass (the pre-existing
cmdKenBurns: curve override changes control pointsassertion was corrected from-0.47to0.47— it had encoded the bug). Aggregate coverage stays above the 80% gate. draft_content.jsonoutput is not byte-identical to 1.1.1 forken-burnswithto − from ≠ -0.5(intended correctness fix; see Changed).
1.1.1 — 2026-05-17
Patch fixing Bug #3, found during end-to-end verification of 1.1.0: the standalone register command failed on copied or moved drafts. Additive bugfix — no breaking changes, draft_content.json output byte-identical.
Fixed
- Bug #3 —
capcut-david register <dir>trusted the target'sdraft_meta_info.jsonfordraft_name/draft_fold_pathinstead of re-deriving them from the directory argument. A draft generated at path A then copied to CapCut path B kept A's name and path, so theroot_meta_info.jsonentry pointed at the wrong location and CapCut couldn't open it. Dedup was also keyed ondraft_id; acp -rkeeps the same id, soregisterreturned{"added": false}and indexed nothing. Fix:registernow derivesdraft_namefrombasename(<dir>)anddraft_fold_pathfrom the resolved absolute<dir>, rewrites the sidecar to match, regenerates a freshdraft_idwhen it collides with a different folder, and dedups ondraft_fold_path. Thepsycho-build --out <dir> --registerone-shot is unaffected (verified — it already writes a correct--out-derived sidecar before registering).
Changed
registeridempotency key changed fromdraft_idtodraft_fold_path(supersedes the "Idempotent ondraft_id" note in 1.1.0). Re-registering the same directory is still a no-op (added: false); registering a different directory that carries a duplicatedraft_idnow succeeds with a freshly generated id instead of being silently skipped.registeralso rewrites the target'sdraft_meta_info.json(draft_name/draft_fold_path/draft_id) so the sidecar stays consistent with the draft's on-disk location.
1.1.0 — 2026-05-17
First minor after 1.0 graduation. Closes the two post-1.0 bugs filed against the published surface: psycho-build drafts now appear in CapCut's UI (Bug #1), and init works out-of-the-box without --template (Bug #2). Additive only — no breaking changes.
Added
capcut-david register <draft-dir> [--projects-root <dir>]— append a built draft to CapCut's<projects-root>/root_meta_info.json.all_draft_store[]so it surfaces in the CapCut UI. Idempotent ondraft_id. Cross-platform default projects-root (Win:%LOCALAPPDATA%/CapCut/User Data/Projects/com.lveditor.draft; macOS:~/Movies/CapCut/User Data/Projects/com.lveditor.draft; Linux best-effort under~/.local/share/CapCut/...).--registerflag onpsycho-build— when passed, runs the equivalent ofregister <draft-dir>after emitting the draft.--projects-root <dir>overrides the default.psycho-buildnow also emitsdraft_meta_info.jsonanddraft_info.jsonalongsidedraft_content.json. These are the two CapCut sidecar files required for the draft to be indexed; without them the draft existed on disk but was invisible in the CapCut UI.src/utils/capcut-paths.ts— shared helpersresolveTemplateDir(),defaultProjectsRoot(),nowUs(). Replaces the per-module template-path resolution previously duplicated inpipeline.tsand shipped broken increate.ts:cmdInit.
Fixed
- Bug #1 —
psycho-buildproduced drafts that were invisible in CapCut's UI. The output directory containeddraft_content.json+assets/but was missingdraft_meta_info.json+draft_info.json, and no entry was created inroot_meta_info.json. Fix:psycho-buildnow always emits the two sidecar metadata files; the--registerflag handles the root-meta indexing. - Bug #2 —
capcut-david init(with no--templateflag) tried to copy../CapCutAPI/template, a path inherited from the upstream Python project that never existed in this fork's distribution. Fix: default template now resolves to the bundledtemplates/minimal/viaresolveTemplateDir(). Default--draftsalso became cross-platform (was hard-coded to macOS~/Movies/CapCut/...).
Changed
PsychoBuildResultinterface (src/commands/pipeline.ts) is additive: new fieldsmetaInfoPath,draftInfoPath,registered,registerRootMetaPath. CLI output forpsycho-buildlikewise gainsmeta_info_path,draft_info_path,registered,register_root_meta_path.Flagsinterface (src/utils/cli.ts) gainsregister?: booleanandprojectsRoot?: string.package.json—version→1.1.0.files,bin,engines, deps unchanged.
Compatibility
- CapCut: same as 1.0.0 (≥ 5.x desktop on Windows + macOS).
- JianYing 6+: unsupported (encrypted draft) — unchanged from 1.0.0.
- Node:
engines.node >= 18(unchanged). - Runtime deps: zero (unchanged).
- 21 new tests (194 total, up from 173). Aggregate coverage on
src/commands/*+src/draft.tsremains above the 80% gate. - All existing CLI commands and on-disk draft shapes are byte-identical to 1.0.0.
draft_content.jsonoutput is unchanged.
1.0.0 — 2026-05-12
Stable release. Full SemVer 2.0.0 guarantees per RELEASE.md §1 now in effect. Code-identical to 0.5.0 and 1.0.0-rc.1 — what changes is the contract.
Highlights
- The fork graduates after 5 implementation phases (A → E):
0.1.0— fork fromrenezander030/capcut-cli, modularsrc/(Phase A)0.2.0— fixture-backednode:testharness, ≥80% coverage, full CI matrix (Phase B)0.3.0— creation primitivesadd-keyframeandken-burns(Phase C)0.4.0—psycho-buildYAML manifest pipeline (Phase D)0.5.0— packaging consolidation: bundledcapcut-davidskill, in-repo schema docs, README rewrite (Phase E)
- 27 CLI commands across 6 families. 173 tests, ≥80% coverage. 3000+ lines of in-repo schema reference. Zero runtime deps. Tarball ~110 kB / 52 files.
Migration from 0.x
- Pinned to
0.4.0or0.5.0→1.0.0: no change. CLI surface byte-identical between0.5.0,1.0.0-rc.1,1.0.0. Just bump the pin. - Pinned to
0.1.0–0.3.0: surface is additive across0.x; nothing was removed. Review use of inspect family (added0.1.0) and creation primitives (added0.3.0). cut-*Claude skills users (cut-audio,cut-draft,cut-motion,cut-storyboard,cut-tiktok): deprecated since2026-05-12withdeprecated: truefrontmatter + redirect callouts pointing at the bundledcapcut-davidskill. Install the new skill from$(npm root -g)/capcut-cli-david/skills/capcut-david/.
Changed
package.json—version→1.0.0. Dist-taglatestadvances to1.0.0(was0.4.0).release-notes/1.0.0.md— finalised from placeholder to the published GitHub release body (Highlights / Migration / Roadmap / Thanks).
Deprecated and removed
- The 5 legacy
cut-*Claude skills are deprecated. Migrate to the unifiedcapcut-davidskill bundled in this package. - Removed nothing else. The full
0.xCLI surface is preserved.
Compatibility
- CapCut: ≥ 5.x desktop (Windows + macOS); both
cutcli-shape (new_version: 167.x) and CapCut-UI shape (169.x) supported. - JianYing 6+: unsupported (encrypted draft) — see
COMPATIBILITY.md§5. - Node:
engines.node >= 18. - Runtime deps: zero (Node stdlib only).
Roadmap (1.x — non-binding)
1.x.0—capcut-david query(animation / sticker / effect / filter catalogue lookup; recipe-referenced)1.x.0—capcut-david validate <project>(schema-invariant linter)1.x.0— JianYing 6+ research;psycho-builddynamic audio ducking
1.0.0-rc.1 — 2026-05-12
Release candidate for 1.0.0. No CLI / API / schema changes vs 0.5.0. Code is identical; this tag is the dress rehearsal for the SemVer 2.0.0 contract taking effect at 1.0.0 per RELEASE.md §1.
Changed
package.json—version→1.0.0-rc.1. Published with--tag next.
Cutover (orchestrator-side, outside the npm package)
- The 5 legacy
cut-*Claude skills (cut-audio,cut-draft,cut-motion,cut-storyboard,cut-tiktok, in the upstream wiki vault) carrydeprecated: truefrontmatter + a redirect callout pointing at the bundledcapcut-davidskill — applied 2026-05-12. Wiki_Claude/SKILLS MAP.mdlistscapcut-davidand the five deprecatedcut-*entries with their migration targets.- Pre-release audit (Explore sub-agent) — verdict SHIP: 7/7 Phase E criteria PASS, cross-links resolve, package.json
filesarray complete, no v0.1.0 stragglers in repo content.
Soak checklist (self-validation against this RC)
-
npm view capcut-cli-david@1.0.0-rc.1returns1.0.0-rc.1; dist-tagnextadvances to it -
capcut-david --helpprints all command families (Overview / Browse / Detail / Create / Edit / Animate / Pipeline) -
capcut-david psycho-build examples/psycho/manifest.example.yaml --out /tmp/test --seed 42succeeds end-to-end - Skill loads in Claude Code from
~/.claude/skills/capcut-david/(copied from$(npm root -g)/capcut-cli-david/skills/capcut-david/) - All 7
docs/draft-schema/files render on GitHub atv1.0.0-rc.1 - Tarball contains
skills/,docs/,release-notes/,templates/,dist/, the 7 root meta-docs (README/CHANGELOG/LICENSE/NOTICE/COMPATIBILITY/UPSTREAM/RELEASE.md) - CI matrix green on commit (13/13 jobs: Node 18/20/22 × Ubuntu/macOS/Windows + Lint+Typecheck + Coverage + Fixture integrity + CI Pass)
If every box stays checked through the soak window, promote to 1.0.0 per RELEASE.md §1 (--tag latest). Otherwise bump to 1.0.0-rc.N+1.
Compatibility
- Unchanged from
0.5.0. CapCut ≥ 5.x desktop. JianYing 6+ unsupported. Node ≥ 18. Runtime deps: zero.
0.5.0 — 2026-05-12
Phase E packaging consolidation — graduation prep. No new commands (feature-frozen per Phase E constraints). The fork ships its own Claude Code skill bundle and an in-repo CapCut draft-schema reference, in service of the imminent 1.0.0 graduation.
Added
skills/capcut-david/SKILL.md— unified Claude Code skill that supersedescut-draft,cut-storyboard,cut-motion,cut-audio, andcut-tiktok(the 5 legacycut-*skills). Bundled with the npm package underskills/.skills/capcut-david/references/recipes-{motion,audio,tiktok,storyboard}.md— full recipes migrated and consolidated from the 5 source skills, normalised against thecapcut-davidcommand surface.docs/draft-schema/(7 files, ~3000 lines) — in-repo reverse-engineered reference for CapCut'sdraft_content.jsonschema: overview, tracks/segments, materials, keyframes & animations, effects/filters/stickers, and version differences.docs/README.md— thin docs index pointing atdraft-schema/and the existing top-level docs (RELEASE.md,UPSTREAM.md,COMPATIBILITY.md).release-notes/— per-version GitHub release-notes drafts (this directory). Each release tag pulls its body from the matching file.COMPATIBILITY.md,UPSTREAM.md,RELEASE.md— meta-docs moved from the upstream planning vault into the repo root so README + CHANGELOG cross-links resolve on GitHub and in the npm tarball.
Changed
README.md— full rewrite: tagline + comparison table (vs upstreamcutcliandcapcut-cli) + Quickstart + command index + pointers toskills/capcut-david/SKILL.mdanddocs/draft-schema/.package.json—version→0.5.0.filesarray extended to shipskills/,docs/,release-notes/,CHANGELOG.md,COMPATIBILITY.md,UPSTREAM.md,RELEASE.mdwith the npm package alongside the existingdist/,templates/,README.md,LICENSE,NOTICE.
Compatibility
- CapCut: same as 0.4.0 (≥ 5.x desktop / JianYing 移动剪辑 ≥ 12.x).
- Node:
engines.node >= 18(unchanged). - Runtime deps: still zero.
0.4.0 — 2026-05-12
Phase D — the psycho-build YAML manifest pipeline. A single command that
composes the existing creation primitives (init + add-video × N +
ken-burns × N + add-audio × 2 + add-text × M) into a complete
TikTok-format (1080×1920) draft. Zero new draft-writing code lives in the
pipeline module — it orchestrates only.
Added
capcut-david psycho-build <manifest.yaml> [--out <dir>] [--seed <n>]— consumes a YAML manifest describing images (each with optionalken_burns), an optional voice track, optional music track (statically ducked viavolume), and optional SRT-driven captions with a style preset. Produces a draft directory ready to open in CapCut.src/commands/pipeline.ts— hand-rolled YAML subset parser (block + flow mappings/sequences, scalars, quoted strings, comments, line-numbered errors), hand-rolled SRT parser (HH:MM:SS,mmm timing, multi-line text, CRLF tolerant, optional index line), manifest validator with field-level error messages, mulberry32 + FNV-1a seeded UUID generator for deterministic builds (RFC 4122 v4 layout).--seed <n>global flag — same seed + same manifest → byte-identical draft. Seed may also live in the manifest underseed:(the CLI flag wins).examples/psycho/manifest.example.yaml+ placeholder assets (assets/img{1,2,3}.jpg,assets/narration.mp3,assets/ambient.mp3,assets/captions.srt) +examples/psycho/README.mdQuickstart.templates/minimal/draft_content.json— bundled empty-draft template forinit-style operations from the pipeline (shipped viapackage.json:files).test/pipeline.test.mjs— 39 new tests (212 total): YAML parser units (scalars, quoted strings, nested blocks, flow values, comments, errors), SRT parser units (3-entry blocks, multiline text, CRLF, missing index, empty input, malformed timestamps), manifest validator coverage (every required field's specific error message), duration parser, seeded UUID determinism, full E2E build vs the example manifest with structural assertions, determinism check (same seed twice → identical draft modulo volatile path fields), different-seed-different-ids check, and three CLI-level happy/error paths.
Changed
--helpadds a newPipeline:section documentingpsycho-build.src/utils/companion.ts—setUuidProvider(fn | null)is new and lets pipeline.ts swaprandomUUIDfor a seeded generator without touching any other module. Existing UUID semantics unchanged when no provider is installed.package.json—filesincludestemplates/so the bundled minimal draft template ships with the npm package.
Compatibility
- CapCut: same as 0.3.0 (≥ 5.x desktop / JianYing 移动剪辑 ≥ 12.x).
- Node:
engines.node >= 18(unchanged). - Runtime deps: still zero. Pipeline is built entirely on Node stdlib.
0.3.0 — 2026-05-12
Phase C — creation primitives: keyframes and Ken Burns. Two new commands, zero behavioral change to existing surface.
Added
capcut-david add-keyframe <project> <id> <time> --property <p> --value <v> [--curve <c>]— generic per-property keyframe insertion. Properties:scale_x,scale_y,position_x,position_y,rotation,alpha. Maintains sort order bytime_offsetinsegment.common_keyframes[*].keyframe_list. A keyframe at an existingtime_offsetis replaced in place. Curves:linear(default),ease-in,ease-out,ease-in-out.capcut-david ken-burns <project> <id> --from <scale> --to <scale> [--curve <c>]— opinionated pairedKFTypeScaleX+KFTypeScaleYkeyframes fromt=0tot=segment.target_timerange.duration. Wipes any existing scale_x / scale_y containers on the segment before writing (deterministic output). Default curve:ease-out(CapCut "Cubic Out" preset). Refuses to act on segments without aclipblock (e.g. audio).test/keyframe.test.mjs— 33 new tests (134 total): 6-property happy paths, insertion ordering, replace-at-same-time, curve override profiles, Ken Burns parity againstken-burns-draft.json(handle ratios 0.32 / −0.4 against keyframe interval, tolerance ±0.001), 13 error paths (missing flags, invalid property, out-of-range alpha, time exceeds duration, invalid curve, audio segment, etc.).
Changed
--helpadds a newKeyframes:section documenting both commands.Flagsinterface (src/utils/cli.ts) addsproperty,value,curve,from,tostring fields. Additive — does not affect any existing command.
Compatibility
- No schema change. Output JSON for
common_keyframes[*]matches the shape intest-fixtures/fixtures/ken-burns-draft.json(container withid/material_id/property_type/keyframe_list, keyframe withid/curveType/time_offset/left_control/right_control/values/string_value/graphID). - Curve handle ratios for
ease-outare empirically backed by the shipped fixture (0.32and-0.4).ease-inandease-in-outuse CSScubic-bezierinterior handles (0.42); fixture verification of those presets is deferred to a future minor. - Zero runtime dependencies preserved.
Coverage
- Aggregate on
src/commands/*+src/draft.ts: 93.70 % lines, 91.67 % functions (above the 80 % gate). dist/commands/keyframe.js: 97.86 % lines.
0.2.0 — 2026-05-12
Stable release of the Phase B test suite. Identical content to 0.2.0-beta.0; promoted to latest after beta soak verified the published tarball + CI gate behave as designed.
See 0.2.0-beta.0 below for the full change list.
0.2.0-beta.0 — 2026-05-12
First beta of the Phase B test suite. No behavioral change to any command — purely additive coverage.
Added
- Fixture-backed
node:testsuite covering every existing command (102 tests total, replaces the 9-test Phase A smoke suite):test/create.test.mjs—init,add-video,add-audio,add-text(14 tests).test/edit.test.mjs—set-text,shift,shift-all,speed,volume,opacity,trim(22 tests).test/inspect.test.mjs—info,tracks,materials,segments,texts,export-srt,segment,material(24 tests).test/template.test.mjs—save-template,apply-template(10 tests).test/cut.test.mjs—cutlong-form → short (9 tests).test/batch.test.mjs— JSONL stdin orchestration (11 tests).
- Shared test helpers under
test/helpers/:load-fixture.mjs—FIXTURESkeys +loadFixture(key)(fresh parse per call).tmp-draft.mjs— deep-clones a fixture intoos.tmpdir(); cleans up viat.after()so tests run isolated.spawn-cli.mjs— spawns the builtdist/index.js, parses JSON stdout + JSON error on stderr.
npm run test:coverage— runsnode --test --experimental-test-coveragewith--test-coverage-lines=80 --test-coverage-functions=80againstdist/commands/**/*.jsanddist/draft.js.- CI: new
coveragejob (Node 22 / ubuntu-latest) wired into theci-passgate.
Coverage
- Aggregate on
src/commands/*+src/draft.ts: 93.03 % lines, 95.65 % functions (well above the 80 % gate). - Per file: batch 100 %, edit 100 %, cut 98.70 %, create 97.76 %, template 93.53 %, draft 89.66 %, inspect 80.41 %.
Notes
- Test files are
.test.mjs(not.test.ts) so the Node 18 cell in the CI matrix can execute them without a TypeScript stripper. Tests import the compiled CLI fromdist/...js, mirroring the existingsmoke.test.mjspattern. - Each test that mutates a draft gets a fresh tmp copy of the fixture; no test mutates the on-disk fixture corpus.
Compatibility
- No behavioral or schema changes. Pure test addition.
- Same Node ≥ 18 / 3-OS support as 0.1.0.
0.1.0 — 2026-05-12
First release of the fork. Baseline = upstream capcut-cli@0.2.2 (commit c922338) restructured into a modular layout. No behavioral change to any existing command.
Added
- Fork attribution surface:
NOTICEfile crediting Rene Zander (upstream) and cutcli inspiration.LICENSEcarries both copyright lines (Rene Zander + David Beles).- README banner naming the upstream and explaining when to use which.
package.jsoncontributors[]field credits Rene Zander.
- Dev tooling:
- Biome 2 for lint + format (
npm run lint,npm run lint:fix). - TypeScript 5.6 typecheck step (
npm run typecheck), separate from build. tsconfig.jsonships declaration files (declaration: true) for downstream type consumers.
- Biome 2 for lint + format (
- CI: GitHub Actions workflow (
.github/workflows/ci.yml) runs lint + typecheck + 3-Node × 3-OS test matrix (Node 18/20/22 × Ubuntu/macOS/Windows) plus fixture-integrity validation. - Test corpus: 9 anonymized CapCut draft fixtures under
test-fixtures/fixtures/(minimal, ken-burns, effects, subtitles, full-psycho, animations, stickers, transitions, masks-filters) +_final_integrity.pycross-reference validator. All 9 pass clean. - Phase A smoke tests in
test/smoke.test.mjs(time-format parsing, srtTime formatting, binary --help, binary info on minimal fixture, binary error on unknown command). Phase B will expand to fixture-backed coverage on every command. - npm package
bin: single namecapcut-david(upstream shipscapcut+capcut-cli).
Changed
- Module layout — split upstream's
src/factory.ts+ monolithicsrc/index.tscmd functions into a typed module tree (no behavior change):src/index.ts— CLI entry, argument parser, dispatch only.src/draft.ts— preserved from upstream (Draft types + load/save/find helpers).src/utils/time.ts— moved fromsrc/time.ts.src/utils/cli.ts— new sharedFlags/CliError/die/out/requireArgs.src/utils/companion.ts— extracted upstream'screateCompanionMaterials,registerCompanions,baseSegment,uuid,hexToRgb.src/commands/create.ts—initDraft,addText,addAudio,addVideo+ CLI wrappers.src/commands/edit.ts—cmdSetText,cmdShift,cmdShiftAll,cmdSpeed,cmdVolume,cmdTrim,cmdOpacity.src/commands/inspect.ts—cmdInfo,cmdTracks,cmdSegments,cmdTexts,cmdMaterials,cmdSegmentDetail,cmdMaterialDetail,cmdExportSrt.src/commands/template.ts—saveTemplate,applyTemplate+ CLI wrappers.src/commands/cut.ts—cutProject+ CLI wrapper.src/commands/batch.ts—cmdBatch+execBatchOp.
- Help text addresses the binary as
capcut-david(wascapcut).
Fixed
batchcommand now reads stdin via fd 0 instead of/dev/stdin, making the command portable to Windows (where/dev/stdindoes not resolve). This is the only deliberate behavioral delta from upstream0.2.2.
Removed (from upstream's tree, none from the runtime contract)
marketplace.jsonand.claude-plugin/(we ship our own Claude Code skill in Phase E; not the upstream's plugin metadata).hooks/andskills/capcut-edit/(upstream's Claude Code plugin assembly; replaced in Phase E).bin/capcut(we expose onlybin: { "capcut-david": "dist/index.js" }).README.zh-CN.md(not maintained on this fork).- Upstream README's Gumroad CTAs and
utm_*tracking parameters (perUPSTREAM.md§5; not a license requirement, but a courtesy not to piggyback on upstream's marketing funnel).
Synced from upstream
- Initial baseline: upstream commit
c922338(v0.2.2, 2026-05-07). Future syncs land via theupstream-syncbranch perUPSTREAM.md§2.
Compatibility
- Tested against CapCut 8.x desktop on Windows + macOS (per
COMPATIBILITY.md§1). - Node ≥ 18; CI matrix covers Node 18, 20, 22.
- JianYing 6+ remains unsupported (encrypted
draft_content.json— seeCOMPATIBILITY.md§5).