cueframe 0.1.0

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.
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ import{A as ne,a as x,b as u,c as v,d as g,e as _,f as S,g as X,h as ee,i as C,j as N,k as te,l as re,m as P,n as A,o as oe,p as L,q as U,r as m,s as y,t as T,u as l,v as q,w as J,x as w,y as j,z as $}from"./chunk-KEYFXLX4.js";import{Command as Mt}from"commander";import{existsSync as se,mkdirSync as Ge,copyFileSync as He,readdirSync as Ve,statSync as Ze}from"fs";import{homedir as Qe}from"os";import{dirname as ie,join as W}from"path";import{fileURLToPath as Ye}from"url";function Xe(){let t=ie(Ye(import.meta.url));for(let e=0;e<8;e++){let r=W(t,"skills");if(se(r)&&Ze(r).isDirectory())return r;let o=ie(t);if(o===t)break;t=o}return null}function ae(t){let e=Xe();e||(g(t,"install",new Error("Bundled skills/ directory not found"),m.UserError),l(m.UserError));let r=[];for(let o of Ve(e)){let n=W(e,o,"SKILL.md");if(!se(n))continue;let i=W(Qe(),".claude","skills",o);Ge(i,{recursive:!0});let c=W(i,"SKILL.md");He(n,c),r.push(c)}r.length===0&&(g(t,"install",new Error("No SKILL.md files found under skills/"),m.UserError),l(m.UserError)),v(t,{ok:!0,count:r.length,installed:r},()=>{console.log(`\u2713 Installed ${r.length} CueFrame skill(s):`);for(let o of r)console.log(` \u2192 ${o}`);console.log("Restart your agent session (Claude Code / Cursor / Codex) to load them.")})}var B=["media","sessions","projects","renders","webhooks"],ce={readonly:Object.fromEntries(B.map(t=>[t,["read"]])),editall:Object.fromEntries(B.map(t=>[t,["read","write"]]))};function et(t){if(t.preset){let e=ce[t.preset];return e||(console.error(`Unknown --preset "${t.preset}". Valid: ${Object.keys(ce).join(", ")}.`),l(m.UserError)),e}if(t.scope)return tt(t.scope);console.error("Provide --scope or --preset (e.g. --preset readonly | editall)."),l(m.UserError)}function tt(t){let e={};for(let r of t.split(",").map(o=>o.trim()).filter(Boolean)){let[o,n]=r.split(":");(!o||!n)&&(console.error(`Invalid --scope "${r}". Use resource:rw (e.g. renders:rw,media:r).`),l(m.UserError)),B.includes(o)||(console.error(`--scope "${r}": unknown resource "${o}". Valid: ${B.join(", ")}.`),l(m.UserError));let i=[];/r/i.test(n)&&i.push("read"),/w/i.test(n)&&i.push("write"),i.length===0&&(console.error(`--scope "${r}": actions must include r and/or w.`),l(m.UserError)),e[o]=i}return Object.keys(e).length===0&&(console.error("--scope is required, e.g. --scope media:rw,projects:rw,renders:rw"),l(m.UserError)),e}async function de(t){w("dev");let e=et(t),r={name:t.name,permissions:e};if(t.expires){let o=Number(t.expires);(!Number.isFinite(o)||o<=0)&&(console.error("--expires must be a positive number of days."),l(m.UserError)),r.expiresIn=Math.round(o*86400)}try{let o=await N("/v1/api-keys",{method:"POST",body:JSON.stringify(r),headers:{"content-type":"application/json"}});v(t,{ok:!0,id:o.id,prefix:o.prefix,start:o.start,key:o.key},()=>{let n=Object.entries(e).map(([i,c])=>`${i}:${c.map(d=>d[0]).join("")}`).join(" ");console.log(`\u2713 API key "${t.name}" created \u2014 scopes: ${n}`),console.log(`
3
+ ${o.key}
4
+ `),console.log(" Shown ONCE \u2014 store it now (paste it into your integration; not saved here).")})}catch(o){g(t,"keys_create",o,m.ApiClientError),l(m.ApiClientError)}}async function le(t){w("dev");let e=await C("/api/auth/api-key/list",{method:"GET"});e.status>=400&&(g(t,"keys_list",new Error(`list failed (${e.status})`),m.ApiClientError),l(m.ApiClientError));let r=Array.isArray(e.body)?e.body:[];v(t,{ok:!0,keys:r},()=>{if(r.length===0){console.log("No API keys.");return}for(let o of r)console.log(` ${o.id} ${o.name??"(unnamed)"} ${o.start??""}\u2026`)})}async function me(t,e){w("dev");let r=await C("/api/auth/api-key/delete",{method:"POST",body:JSON.stringify({keyId:t}),headers:{"content-type":"application/json"}});r.status>=400&&(g(e,"keys_revoke",new Error(`revoke failed (${r.status})`),m.ApiClientError),l(m.ApiClientError)),v(e,{ok:!0,revoked:t},()=>console.log(`\u2713 Revoked ${t}`))}async function ue(t){w(t.target),u(t,"list_prepare",{target:t.target});try{let e=await A.listProjects(),r=t.limit,o=typeof r=="number"&&Number.isFinite(r)&&r>0?e.data.slice(0,r):e.data;if(u(t,"list_complete",{count:o.length,total:e.data.length,truncated:o.length<e.data.length,projects:o}),!t.json){if(e.data.length===0){console.log("No projects found.");return}for(let n of o){let i=new Date(n.createdAt).toLocaleDateString();console.log(` ${n.id} ${n.name} (${i})`)}o.length<e.data.length&&console.log(` \u2026 ${e.data.length-o.length} more (raise --limit to see all)`)}}catch(e){g(t,"list",e,y(e)),t.json||console.error(`Failed to list projects: ${j(e)}`),l(y(e))}}import{readFile as rt,stat as ot}from"fs/promises";import{existsSync as nt}from"fs";import{basename as it,extname as st}from"path";async function pe(t){let{file:e,signingUrl:r,token:o,metadata:n,onProgress:i}=t,c=r.replace(/\/$/,""),d={"uppy-auth-token":o,"Content-Type":"application/json"},f=await fetch(`${c}/s3/multipart`,{method:"POST",headers:d,body:JSON.stringify({filename:e.name,type:e.type||"video/mp4",metadata:{uploadToken:o,...n}})});if(!f.ok)throw new Error(`Upload init failed: ${f.status}`);let{uploadId:p}=await f.json(),s=Math.ceil(e.size/10485760),a=[];for(let E=0;E<s;E++){let b=E*10485760,k=Math.min(b+10485760,e.size),O=e.slice(b,k),I=E+1,R=await fetch(`${c}/s3/multipart/${p}/${I}`,{headers:{"uppy-auth-token":o}});if(!R.ok)throw new Error(`Presign failed for part ${I}: ${R.status}`);let{url:D}=await R.json(),V=await fetch(D,{method:"PUT",body:O});if(!V.ok)throw new Error(`Part ${I} upload failed: ${V.status}`);a.push({PartNumber:I,ETag:V.headers.get("ETag")??""}),i?.(k/e.size)}let h=await fetch(`${c}/s3/multipart/${p}/complete`,{method:"POST",headers:d,body:JSON.stringify({parts:a})});if(!h.ok)throw new Error(`Complete failed: ${h.status}`);return await h.json()}var at=4e3,ge=.1;function ct(t){switch(st(t).toLowerCase()){case".mp4":return"video/mp4";case".mov":return"video/quicktime";case".webm":return"video/webm";case".mkv":return"video/x-matroska";default:return"video/mp4"}}async function fe(t,e){nt(t)||(g(e,"preflight",new Error(`File not found: ${t}`),m.UserError),e.json||console.error(`File not found: ${t}`),l(m.UserError));let r=it(t),o=(await ot(t)).size,n=ct(r);if(e.dryRun){u(e,"upload_dry_run",{file:r,sizeBytes:o,contentType:n,target:e.target,analyzeAfter:!!e.analyze,wouldCall:"POST /v1/media \u2192 multipart upload \u2192 poll processing"}),e.json||console.log(`[dry-run] Would upload ${r} (${(o/1024/1024).toFixed(1)} MB, ${n}) to ${e.target}.`);return}w(e.target),u(e,"upload_prepare",{file:r,sizeBytes:o,contentType:n,target:e.target});let i=x("Preparing upload\u2026",e.json),c,d,f;try{let b=await P.createMediaUpload({filename:r,contentType:n,size:o});c=b.id,d=b.upload.endpoint,f=b.upload.token,i.succeed("Ready"),u(e,"upload_ready",{mediaItemId:c,endpoint:d,tokenIssued:!0})}catch(b){i.fail(`Failed: ${j(b)}`),g(e,"upload_prepare",b,y(b)),l(y(b))}let p=Date.now(),s=x(`Uploading ${r} (${(o/1024/1024).toFixed(1)} MB)\u2026`,e.json);try{let b=await rt(t),k=new File([b],r,{type:n}),O=0;await pe({file:k,signingUrl:d,token:f,metadata:{convexMediaItemId:c},onProgress:I=>{s.text=`Uploading ${r} \xB7 ${(I*100).toFixed(0)}%`;let R=Math.floor(I/ge)*ge;R>O&&R<1&&(O=R,u(e,"upload_progress",{percent:Math.round(R*100)}))}}),s.succeed(`Uploaded: ${r}`),u(e,"upload_complete",{mediaItemId:c,bytesUploaded:o,durationMs:Date.now()-p})}catch(b){s.fail(`Upload failed: ${j(b)}`),g(e,"upload",b,m.NetworkError),l(m.NetworkError)}let a=x("Processing (transcription)\u2026",e.json),h=Date.now(),E=null;for(;;){await new Promise(b=>setTimeout(b,at));try{let b=await P.getMedia(c),k=b?.processingStatus?.phase??"unknown",O=b?.processingStatus?.percent??null,I=Math.round((Date.now()-h)/1e3),R=O!=null?` \xB7 ${Math.round(O*100)}%`:"";if(a.text=`Processing\u2026 ${k}${R} (${I}s)`,u(e,"process_progress",{phase:k,percent:O,elapsedSec:I}),k==="complete"||k==="completed"){a.succeed(`Ready: ${c}`),E=b,u(e,"process_complete",{mediaItemId:c,durationMs:Date.now()-h,item:b});break}if(k==="error"){let D=b?.processingStatus?.error??"unknown error";a.fail(`Processing failed: ${D}`),g(e,"process",new Error(D),m.ApiServerError),l(m.ApiServerError)}}catch{}}if(e.analyze){_(`
5
+ Media item ID: ${c}`,e);let{analyzeCommand:b}=await import("./analyze-JQBFZK57.js");await b(c,{target:e.target,wait:!0,json:e.json});return}e.json||v(e,{mediaItemId:c,item:E},()=>{console.log(`
6
+ Media item ID: ${c}`),console.log(`
7
+ Run \`cueframe analyze ${c} --target ${e.target}\` to generate clip suggestions.`)})}function Z(t){let e=Math.floor(t/60),r=Math.floor(t%60);return`${e}:${String(r).padStart(2,"0")}`}async function he(t,e){t!==void 0&&$(t,"mediaItemId",e),w(e.target),u(e,"clips_prepare",{target:e.target,mode:t?"suggestions":"media-list",...t?{mediaItemId:t}:{}});try{if(!t){let o=await P.listMedia();if(u(e,"clips_complete",{mode:"media-list",count:o.data.length,media:o.data}),!e.json){if(o.data.length===0){console.log("No media items found. Import a video first.");return}console.log("Media items (pass the ID to `cueframe clips <id>`):"),console.log("");for(let n of o.data){let i=n.duration?`${Z(n.duration)} `:"",c=n.processingStatus?.phase??"unknown",d=c==="complete"?"":` [${c}]`;console.log(` ${n.id} ${n.name} ${i}${d}`)}}return}let r=await P.listMediaSuggestions(t);if(u(e,"clips_complete",{mode:"suggestions",mediaItemId:t,count:r.suggestions.length,suggestions:r.suggestions}),!e.json){if(r.suggestions.length===0){console.log("No clip suggestions found for this media item."),console.log("If analysis is still running, try again shortly. Otherwise the video may not have suitable clips.");return}console.log(`${r.suggestions.length} clip suggestion${r.suggestions.length!==1?"s":""}:`),console.log("");for(let o of r.suggestions){let n=`${Z(o.trim.start)} \u2192 ${Z(o.trim.end)}`,i=(o.trim.end-o.trim.start).toFixed(1);console.log(` ${o.id}`),console.log(` ${o.title}`),console.log(` ${n} (${i}s) ${o.format} score: ${o.score}`),o.reason&&console.log(` ${o.reason}`),console.log("")}}}catch(r){g(e,"clips",r,y(r)),e.json||console.error(`Failed: ${j(r)}`),l(y(r))}}import{readFileSync as dt}from"fs";async function ye(t,e){$(t,"projectId",e),w(e.target),u(e,"composition_get_prepare",{projectId:t});let r=await M(`/v1/projects/${t}/composition`,{method:"GET"},e,"composition_get");r.status>=400&&(F(r,e,"composition_get"),l(T(r.status)));let o=r.headers.etag,n=o&&je(o);u(e,"composition_get_complete",{projectId:t,etag:n,status:r.status}),v(e,{etag:n,composition:r.body},()=>{n&&console.log(`# ETag: ${n}`),console.log(JSON.stringify(r.body,null,2))})}async function we(t,e){$(t,"projectId",e),e.body||(console.error("--body is required (inline JSON, @file.json, or @- for stdin)"),l(m.UserError)),w(e.target);let r=await Q(t,e),o=await Ee(e.body);u(e,"composition_put_prepare",{projectId:t,ifMatch:r});let n=new Headers({"content-type":"application/json","if-match":r}),i=await M(`/v1/projects/${t}/composition`,{method:"PUT",headers:n,body:o},e,"composition_put");i.status>=400&&(F(i,e,"composition_put"),l(T(i.status)));let c=i.headers.etag;u(e,"composition_put_complete",{projectId:t,etag:c,status:i.status,response:i.body}),v(e,{etag:c,response:i.body},()=>{console.log(`Composition saved (ETag: ${c??"unknown"}).`),i.body&&typeof i.body=="object"&&console.log(JSON.stringify(i.body,null,2))})}async function z(t,e){$(t,"projectId",e),e.suggestionId||(console.error("--suggestion <suggestionId> is required"),l(m.UserError)),w(e.target);let r=await Q(t,e),o;if(e.format)try{o=JSON.parse(e.format)}catch{console.error(`--format must be JSON, e.g. '{"aspectRatio":"9:16","fps":30,"resolution":"fhd"}'`),l(m.UserError)}let n=JSON.stringify({suggestionId:e.suggestionId,...o?{format:o}:{}});u(e,"composition_from_suggestion_prepare",{projectId:t,suggestionId:e.suggestionId,ifMatch:r});let i,c=r,d=0,f=3;for(;;){d++;let s=new Headers({"content-type":"application/json","if-match":c});if(i=await M(`/v1/projects/${t}/composition/from-suggestion`,{method:"POST",headers:s,body:n},e,"composition_from_suggestion"),!(i.status===409&&typeof i.body=="object"&&i.body!==null&&i.body.code==="stale_etag"))break;if(d>f){u(e,"composition_from_suggestion_etag_retries_exhausted",{projectId:t,attempts:f});break}u(e,"composition_from_suggestion_etag_retry",{projectId:t,attempt:d,reason:"stale_etag"}),c=await Q(t,e)}i.status>=400&&(F(i,e,"composition_from_suggestion"),l(T(i.status)));let p=i.headers.etag;u(e,"composition_from_suggestion_complete",{projectId:t,suggestionId:e.suggestionId,etag:p,status:i.status,response:i.body,attempts:d}),v(e,{etag:p,response:i.body},()=>{console.log(`Composition built from suggestion ${e.suggestionId} (ETag: ${p??"unknown"}).`),i.body&&typeof i.body=="object"&&console.log(JSON.stringify(i.body,null,2))})}async function be(t){t.body||(console.error("--body is required (inline JSON, @file.json, or @- for stdin)"),l(m.UserError)),w(t.target);let e=await Ee(t.body);u(t,"composition_validate_prepare",{});let r=new Headers({"content-type":"application/json"}),o=await M("/v1/composition/validate",{method:"POST",headers:r,body:e},t,"composition_validate");o.status>=400&&(F(o,t,"composition_validate"),l(T(o.status))),u(t,"composition_validate_complete",{status:o.status}),v(t,{valid:!0},()=>{console.log("\u2713 Composition is valid.")})}function je(t){return t.replace(/^W\//,"").replace(/-gzip(?=("?)$)/,"")}async function Q(t,e){if(e.force)return"*";if(e.ifMatch)return e.ifMatch;let r=await M(`/v1/projects/${t}/composition`,{method:"GET"},e,"composition_etag_fetch");if(r.status===404)return _("No existing composition; treating as create (If-Match: *).",e),"*";r.status>=400&&(F(r,e,"composition_etag_fetch"),l(T(r.status)));let o=r.headers.etag;return o||(g(e,"composition_etag_fetch",new Error("Server returned no ETag on composition GET; cannot safely thread If-Match. Pass --force to overwrite anyway."),m.ApiServerError),l(m.ApiServerError)),je(o)}async function Ee(t){if(t.startsWith("@")){let e=t.slice(1);if(e==="-"){let r=[];for await(let o of process.stdin)r.push(Buffer.isBuffer(o)?o:Buffer.from(o));return Buffer.concat(r).toString("utf-8")}return dt(e,"utf-8")}return t}async function M(t,e,r,o){try{return await C(t,e)}catch(n){g(r,o,n,m.NetworkError),l(m.NetworkError)}}function F(t,e,r){let o=t.body?.error,n=o?.code??"unknown",i=o?.message??t.statusText,c=lt(n,t.status,o?.details),d=T(t.status);e.json?g(e,r,Object.assign(new Error(i),{code:n,status:t.status,details:o?.details,advice:c}),d):(process.stderr.write(`\u2716 ${r}: ${i} (${n}, ${t.status})
8
+ `),c&&process.stderr.write(` \u2192 ${c}
9
+ `))}function lt(t,e,r){let o=new S(e,{code:t,message:"",details:r&&typeof r=="object"?r:void 0}),n=te(o);return n||(e===404?"Project or composition not found. Run `cueframe list` to confirm the project ID; the composition may not yet exist.":null)}async function K(t,e){let r=e.map(c=>c==null?"null":String(c)).join(":"),o=`${t}:${r}`,n=new TextEncoder().encode(o),i=await crypto.subtle.digest("SHA-256",n);return Array.from(new Uint8Array(i),c=>c.toString(16).padStart(2,"0")).join("")}var ve=1500,mt=1800*1e3;async function*ut(t,e,r){let o=ee();if(!o?.baseUrl)throw new Error("cueframe: createClient({baseUrl}) must be called before iterComposeEvents.");let n=o.baseUrl.replace(/\/$/,"")+`/v1/projects/${encodeURIComponent(t)}/compose/jobs/${encodeURIComponent(e)}/stream`,i=new Headers({accept:"text/event-stream",...o.headers??{}});o.apiKey&&!i.has("authorization")&&i.set("authorization",`Bearer ${o.apiKey}`);let d=await(o.fetch??fetch)(n,{method:"GET",headers:i,signal:r});if(!d.ok){let a;try{a=(await d.json()).error??{code:"unknown",message:d.statusText}}catch{a={code:"unknown",message:d.statusText}}throw new S(d.status,a,d.headers.get("x-request-id")??void 0)}if(!d.body)throw new Error("SSE response has no body");let f=d.body.getReader(),p=new TextDecoder("utf-8"),s="";try{for(;;){let{value:a,done:h}=await f.read();if(h)break;s+=p.decode(a,{stream:!0});let E=s.indexOf(`
10
+
11
+ `);for(;E!==-1;){let b=s.slice(0,E);s=s.slice(E+2);let k=pt(b);k&&(yield k),E=s.indexOf(`
12
+
13
+ `)}}}finally{try{f.releaseLock()}catch{}}}function pt(t){if(!t.trim())return null;let e=null,r=[];for(let n of t.split(`
14
+ `)){if(!n||n.startsWith(":"))continue;let i=n.indexOf(":");if(i===-1)continue;let c=n.slice(0,i),d=n.slice(i+1).replace(/^ /,"");c==="event"?e=d:c==="data"&&r.push(d)}if(!e||r.length===0||e!=="compose_event")return null;let o;try{o=JSON.parse(r.join(`
15
+ `))}catch{return null}return{event:e,data:o}}async function gt(t,e,r={}){let o=Date.now()+(r.timeoutMs??mt);for(;Date.now()<o;){let n=null;try{for await(let i of ut(t,e)){if(r.onTypedEvent?.(i.data),i.data.event==="job_completed"){let c=i.data.data;n={kind:"complete",compositionId:c.compositionId??null,etag:c.etag??null};break}if(i.data.event==="job_failed"){let d=i.data.data.reason??"compose_failed";d==="cancelled"||d.startsWith("cancelled")?n={kind:"cancelled",reason:d}:n={kind:"error",message:d};break}}}catch(i){if(i instanceof S)throw i;await new Promise(c=>setTimeout(c,ve));continue}if(n)return n;await new Promise(i=>setTimeout(i,ve))}return{kind:"error",message:`Timed out waiting for compose to reach terminal state. ComposeJob: ${e}, Project: ${t}.`}}async function $e(t,e){$(t,"suggestionId",e),$(e.project,"projectId",e),w(e.target),u(e,"compose_prepare",{suggestionId:t,projectId:e.project,target:e.target});let r=e.idempotencyKey??await K("compose",[e.project,t,e.seed??"auto"]);u(e,"idempotency_key",{scope:"compose",key:r,overridden:!!e.idempotencyKey,seed:e.seed??null});let o=x(`Starting compose for ${t}\u2026`,e.json),n,i=!1;try{let p={suggestionId:t};if(e.critique!==void 0&&(p.critique=e.critique),e.clientNonce!==void 0&&(p.clientNonce=e.clientNonce),e.baseline!==void 0&&(p.parentJobId=e.baseline),e.maxCandidates!==void 0&&(p.maxCandidates=e.maxCandidates),e.seed!==void 0){let a=Number(e.seed);Number.isInteger(a)&&(p.seed=a)}let s=await C(`/v1/projects/${e.project}/compose`,{method:"POST",headers:{"content-type":"application/json","idempotency-key":r},body:JSON.stringify(p)});if(s.status>=400){let a=s.body?.error,h=a?.message??s.statusText;throw new S(s.status,{code:a?.code??"unknown",message:h})}n=s.body,i=s.headers["idempotency-replay"]==="true"}catch(p){o.fail(`Failed to start compose: ${j(p)}`),g(e,"compose_create",p,y(p)),l(y(p))}i?(u(e,"compose_idempotent_replay",{composeJobId:n.composeJobId,projectId:n.projectId,workflowId:n.workflowId,idempotencyKey:r}),o.text=`Compose already in flight or complete (job ${n.composeJobId})\u2026`):(u(e,"compose_started",{composeJobId:n.composeJobId,projectId:n.projectId,workflowId:n.workflowId}),o.text=`Composing (job ${n.composeJobId})\u2026`);let c,d,f=Date.now();try{c=await gt(e.project,n.composeJobId,{onTypedEvent:p=>{if(u(e,`compose:${p.event}`,{composeJobId:n.composeJobId,...p.data,timestamp:p.timestamp}),e.json)return;let s=Math.round((Date.now()-f)/1e3),a=p.data;if(p.event==="job_started")o.text=`Composing\u2026 ${a.candidateCount??"?"} candidates (${s}s)`;else if(p.event==="candidate_phase_started")o.text=`Candidate ${a.candidateIndex} starting (${s}s)`;else if(p.event==="candidate_phase_evaluated"){let h=a.perCriterion,E=h?`E${(h.editorial?.score??0).toFixed(1)} S${(h.spatial?.score??0).toFixed(1)} B${(h.brand?.score??0).toFixed(1)} C${(h.caption?.score??0).toFixed(1)}`:"";o.text=`Candidate ${a.candidateIndex} evaluated \xB7 ${E} (${s}s)`}else if(p.event==="critique_retry_started")o.text=`Candidate ${a.candidateIndex} retry ${a.attemptNumber} (${s}s)`;else if(p.event==="candidate_completed"){let h=typeof a.composite=="number"?a.composite.toFixed(2):"?";o.text=`Candidate ${a.candidateIndex} done \xB7 composite ${h} (${s}s)`}else if(p.event==="winner_selected"){let h=typeof a.composite=="number"?a.composite.toFixed(2):"?";(a.route==="publish"||a.route==="handoff")&&(d=a.route);let E=d==="handoff"?" \xB7 \u2691 handoff":d==="publish"?" \xB7 \u2713 publish":"";o.text=`Picked candidate ${a.candidateIndex} \xB7 composite ${h}${E} (${s}s)`}}})}catch(p){o.fail(`Compose watch failed: ${j(p)}`),g(e,"compose_watch",p,y(p)),l(y(p))}if(c.kind==="complete"){o.succeed(`Compose complete (job ${n.composeJobId})`),u(e,"compose_complete",{composeJobId:n.composeJobId,projectId:e.project,compositionId:c.compositionId,etag:c.etag}),e.json||(console.log(`
16
+ Project: ${e.project}`),c.compositionId&&console.log(` Composition: ${c.compositionId}`),c.etag&&console.log(` ETag: ${c.etag}`),d==="publish"?console.log(" Verdict: \u2713 publish \u2014 clears the quality bar, ships autonomously"):d==="handoff"&&console.log(" Verdict: \u2691 handoff \u2014 below bar / floor violation; review in your NLE before publishing"),console.log(`
17
+ \u2192 Render with: cueframe render ${e.project} -t ${e.target}`));return}c.kind==="cancelled"&&(o.warn(`Compose cancelled: ${c.reason}`),u(e,"compose_cancelled",{composeJobId:n.composeJobId,reason:c.reason}),l(m.UserError)),o.fail(`Compose failed: ${c.message}`),g(e,"compose_run",new Error(c.message),m.ApiServerError),l(m.ApiServerError)}async function xe(t){let e=t.aspect?{aspectRatio:t.aspect,fps:t.fps??30,resolution:t.resolution??"fhd"}:void 0;if(t.dryRun){if(u(t,"project_create_dry_run",{name:t.name,target:t.target,format:e??null,fromSuggestion:t.fromSuggestion??null,cheap:t.cheap??!1,wouldCall:t.fromSuggestion?t.cheap?"POST /v1/projects \u2192 POST /v1/projects/:id/composition/from-suggestion (sync translator)":"POST /v1/projects \u2192 POST /v1/projects/:id/compose (async) \u2192 SSE watch":"POST /v1/projects"}),!t.json){let n=t.cheap?" (--cheap: deterministic translator)":"";console.log(`[dry-run] Would create project "${t.name}" on ${t.target}`+(t.fromSuggestion?` from suggestion ${t.fromSuggestion}${n}.`:"."))}return}w(t.target),u(t,"project_create_prepare",{name:t.name,target:t.target,fromSuggestion:t.fromSuggestion??null,cheap:t.cheap??!1});let r=x(`Creating project "${t.name}"\u2026`,t.json),o;try{o=await A.createProject({name:t.name,format:e}),r.succeed(`Created: ${o.id}`)}catch(n){r.fail(`Failed: ${j(n)}`),g(t,"project_create",n,y(n)),l(y(n))}if(u(t,"project_create_complete",{project:o}),!t.json&&!t.fromSuggestion&&console.log(`
18
+ ${o.id} ${o.name}`),t.fromSuggestion&&(t.cheap?await z(o.id,{target:t.target,suggestionId:t.fromSuggestion,json:t.json,force:!0}):await $e(t.fromSuggestion,{target:t.target,project:o.id,json:t.json}),!t.json)){let n=t.cheap?"(--cheap)":"(composed)";console.log(`
19
+ ${o.id} ${o.name} \u2190 composed from ${t.fromSuggestion} ${n}`)}}async function Ce(t,e){$(t,"projectId",e),w(e.target),u(e,"project_delete_prepare",{projectId:t});try{await A.deleteProject(t),u(e,"project_delete_complete",{projectId:t}),v(e,{ok:!0,projectId:t},()=>{console.log(`Project ${t} deleted.`)})}catch(r){g(e,"project_delete",r,y(r)),e.json||console.error(`\u2716 project delete: ${j(r)}`),l(y(r))}}async function ke(t,e){$(t,"mediaId",e),w(e.target),u(e,"media_delete_prepare",{mediaId:t});try{await P.deleteMedia(t),u(e,"media_delete_complete",{mediaId:t}),v(e,{ok:!0,mediaId:t},()=>{console.log(`Media ${t} deleted.`)})}catch(r){g(e,"media_delete",r,y(r)),e.json||console.error(`\u2716 media delete: ${j(r)}`),l(y(r))}}import{writeFileSync as ft}from"fs";import{resolve as ht}from"path";var Se=1500,yt=4e3,wt=1800*1e3;async function G(t,e,r={}){let o=Date.now()+(r.timeoutMs??wt),n=Date.now();for(;Date.now()<o;){let i=null,c=!1;try{for await(let d of re(t,e)){if(d.event==="progress"){let f=Math.round((Date.now()-n)/1e3);if(r.spinner){let p=Math.round(d.data.percent*100);r.spinner.text=`Rendering\u2026 ${d.data.phase}${p>0?` \xB7 ${p}%`:""} (${f}s)`}r.onProgress?.({phase:d.data.phase,percent:d.data.percent,elapsedSec:f});continue}d.event==="complete"?i={kind:"complete",outputUrl:d.data.outputUrl,outputExpiresAt:d.data.outputExpiresAt,duration:d.data.duration}:i={kind:"failed",message:d.data.message};break}}catch(d){if(d instanceof S)c=!0;else{await new Promise(f=>setTimeout(f,Se));continue}}if(i)return i;if(c){let d=await bt(t,e);if(d)return d;await new Promise(f=>setTimeout(f,yt));continue}await new Promise(d=>setTimeout(d,Se))}return{kind:"failed",message:`Timed out waiting for render to reach a terminal state. Render ID: ${e}, Project ID: ${t}.`}}async function bt(t,e){let r=await oe.getRender(t,e);return r.status==="complete"?{kind:"complete",outputUrl:r.outputUrl??null,outputExpiresAt:null,duration:null}:r.status==="error"||r.status==="cancelled"?{kind:"failed",message:r.error??`Render ${r.status}`}:null}async function _e(t,e){$(t,"renderId",e),$(e.project,"projectId",e),w(e.target),u(e,"render_watch_prepare",{renderId:t,projectId:e.project,target:e.target});let r=x(`Watching render ${t}\u2026`,e.json),o;try{o=await G(e.project,t,{spinner:r,onProgress:i=>u(e,"render_progress",{renderId:t,...i})})}catch(i){r.fail(`Failed to watch render: ${j(i)}`),g(e,"render_watch",i,y(i)),l(y(i))}o.kind==="failed"&&(r.fail(o.message),g(e,"render_watch",new Error(o.message),m.ApiServerError),l(m.ApiServerError)),r.succeed(`Render complete (${t})`);let n=null;e.download&&o.outputUrl?n=await H(o.outputUrl,e.out??`./reel-${t.slice(0,8)}.mp4`,e.json):o.outputUrl||(g(e,"render_watch",new Error("Render reported complete but no outputUrl was returned."),m.ApiServerError),e.json||console.error(`Render reported complete but no outputUrl was returned.
20
+ Re-fetch via GET /v1/projects/${e.project}/renders/${t}.`),l(m.ApiServerError)),u(e,"render_complete",{renderId:t,projectId:e.project,outputUrl:o.outputUrl,outputExpiresAt:o.outputExpiresAt,duration:o.duration,outputPath:n}),e.json||(o.outputUrl&&console.log(`MP4 URL: ${o.outputUrl}`),o.outputExpiresAt&&console.log(`Expires: ${o.outputExpiresAt}`),o.duration!=null&&console.log(`Duration: ${o.duration}s`))}async function H(t,e,r){let o=ht(e),n=x(`Downloading \u2192 ${o}\u2026`,r);try{let i=await fetch(t);if(!i.ok)throw new Error(`HTTP ${i.status}`);let c=Buffer.from(await i.arrayBuffer());return ft(o,c),n.succeed(`Saved: ${o} (${(c.length/1024/1024).toFixed(1)} MB)`),o}catch(i){n.fail(`Download failed: ${j(i)}`),console.error(`
21
+ MP4 URL (1-hour TTL): ${t}`),l(m.NetworkError)}}async function jt(t){let e=await C(`/v1/projects/${t}/composition`,{method:"GET"});return e.status===404?"no-composition":e.status>=400?"unknown-composition":e.headers.etag??"no-etag"}async function Ie(t,e){if($(t,"projectId",e),e.dryRun){u(e,"render_dry_run",{projectId:t,target:e.target,wouldCall:"POST /v1/projects/:id/renders \u2192 SSE watch \u2192 download MP4"}),e.json||console.log(`[dry-run] Would render project ${t} on ${e.target}.`);return}w(e.target),u(e,"render_prepare",{projectId:t,target:e.target});let r;try{r=e.idempotencyKey??await K("render",[t,await jt(t)]),u(e,"idempotency_key",{scope:"render",key:r,overridden:!!e.idempotencyKey})}catch(s){r="",u(e,"idempotency_key",{scope:"render",key:null,skipped:!0,reason:s instanceof Error?s.message:String(s)})}let o=x("Queuing render\u2026",e.json),n,i,c=!1;try{let s=e.aspect?{format:{aspectRatio:e.aspect,fps:e.fps??30,resolution:e.resolution??"fhd"}}:{};e.priority&&(s.priority=e.priority),e.intent&&(s.intent=e.intent);let a={"content-type":"application/json"};r&&(a["idempotency-key"]=r);let h=await C(`/v1/projects/${t}/renders`,{method:"POST",headers:a,body:JSON.stringify(s)});if(h.status>=400){let b=h.body?.error;throw new S(h.status,{code:b?.code??"unknown",message:b?.message??h.statusText})}let E=h.body;n=E.id,i=E.status,c=h.headers["idempotency-replay"]==="true",o.succeed(c?`Render replayed: ${n} (status: ${i})`:`Render queued: ${n}`)}catch(s){o.fail(`Failed: ${j(s)}`),g(e,"render_prepare",s,y(s)),l(y(s))}c?u(e,"render_idempotent_replay",{renderId:n,projectId:t,status:i,idempotencyKey:r}):u(e,"render_queued",{renderId:n,projectId:t});let d=x("Rendering\u2026",e.json),f;try{f=await G(t,n,{spinner:d,onProgress:s=>u(e,"render_progress",{renderId:n,...s})})}catch(s){d.fail(`Failed to watch render: ${j(s)}`),g(e,"render_progress",s,y(s)),l(y(s))}f.kind==="failed"&&(d.fail(f.message),g(e,"render_progress",new Error(f.message),m.ApiServerError),l(m.ApiServerError)),d.succeed("Render complete"),f.outputUrl||(g(e,"render_complete",new Error("Render reported complete but no outputUrl was returned."),m.ApiServerError),e.json||console.error(`Render reported complete but no outputUrl was returned.
22
+ Render ID: ${n}
23
+ Project ID: ${t}
24
+ Re-fetch via GET /v1/projects/${t}/renders/${n} once the URL is signed.`),l(m.ApiServerError));let p=await H(f.outputUrl,e.out??`./reel-${n.slice(0,8)}.mp4`,e.json);u(e,"render_complete",{renderId:n,projectId:t,outputUrl:f.outputUrl,outputExpiresAt:f.outputExpiresAt,duration:f.duration,outputPath:p})}import Et from"open";import{createAuthClient as vt}from"better-auth/client";import{deviceAuthorizationClient as $t}from"better-auth/client/plugins";async function Re(t){let e=q(t.target),r=vt({baseURL:e,plugins:[$t()]}),o=x("Requesting device code\u2026",t.json),n;try{let{data:p,error:s}=await r.device.code({client_id:"cueframe-cli",scope:"openid profile email"});(s||!p)&&(o.fail(`Could not start login: ${s?.message??"unknown error"}`),l(m.NetworkError)),n=p}catch(p){o.fail(`Could not start login: ${p instanceof Error?p.message:String(p)}`),l(m.NetworkError)}o.succeed("Device code issued");let i=n.verification_uri_complete??n.verification_uri;_("",t),_(` Visit: ${i}`,t),_(` Code: ${n.user_code}`,t),_("",t);try{await Et(i)}catch{}let c=x("Waiting for approval in browser\u2026",t.json),d=Date.now()+n.expires_in*1e3,f=n.interval*1e3;for(;Date.now()<d;){await new Promise(p=>setTimeout(p,f));try{let{data:p,error:s}=await r.device.token({grant_type:"urn:ietf:params:oauth:grant-type:device_code",device_code:n.device_code,client_id:"cueframe-cli"});if(p?.access_token){let E=p.access_token;J(E,t.target,t.url),c.succeed(`Logged in. Token saved for target "${t.target}".`),v(t,{ok:!0,target:t.target,apiUrl:e},()=>{});return}let a=p??s,h=a?.error;h==="authorization_pending"||(h==="slow_down"?f+=5e3:h==="access_denied"?(c.fail("Access denied in the browser."),l(m.UserError)):h==="expired_token"?(c.fail("Device code expired before approval. Run `cueframe login` again."),l(m.UserError)):h&&(c.fail(`Login failed: ${a?.error_description??h}`),l(m.ApiClientError)))}catch{}}c.fail("Timed out waiting for approval. Run `cueframe login` again."),l(m.UserError)}import{readFileSync as xt}from"fs";var Te=new Set(["GET","POST","PUT","PATCH","DELETE"]);async function Ae(t,e,r){let o=t.toUpperCase();Te.has(o)||(console.error(`Unsupported method: ${t}. Allowed: ${[...Te].join(", ")}.`),l(m.UserError)),e.startsWith("/")||(console.error(`Path must start with '/'. Got: ${e}`),l(m.UserError)),w(r.target);let n=St(r.header??[]),i=await _t(r.body,o);i!==void 0&&!n.has("content-type")&&n.set("content-type","application/json");let c;try{c=await C(e,{method:o,headers:n,body:i})}catch(d){console.error(d instanceof Error?d.message:String(d)),l(m.NetworkError)}kt(c,{include:!!r.include,json:!!r.json,method:o,path:e}),l(Ct(c.status))}function Ct(t){return t===402?m.PaymentRequired:t>=500?m.ApiServerError:t>=400?m.ApiClientError:m.Success}function kt(t,{include:e,json:r,method:o,path:n}){if(r){console.log(JSON.stringify({status:t.status,statusText:t.statusText,headers:t.headers,body:t.body}));return}if(e){process.stdout.write(`HTTP/1.1 ${t.status} ${t.statusText}
25
+ `);for(let[i,c]of Object.entries(t.headers))process.stdout.write(`${i}: ${c}
26
+ `);process.stdout.write(`
27
+ `),Oe(t.body,t.status>=400);return}if(t.status===204||t.body===null){console.error(`${o} ${n} \u2192 ${t.status} (no content)`);return}Oe(t.body,t.status>=400)}function Oe(t,e){let r=e?process.stderr:process.stdout;t!=null&&(typeof t=="string"?r.write(t.endsWith(`
28
+ `)?t:t+`
29
+ `):r.write(JSON.stringify(t,null,2)+`
30
+ `))}function St(t){let e=new Headers;for(let r of t){let o=r.indexOf(":");o===-1&&(console.error(`Invalid --header (expected "Name: value"): ${r}`),l(m.UserError));let n=r.slice(0,o).trim(),i=r.slice(o+1).trim();e.set(n,i)}return e}async function _t(t,e){if(t){if(t.startsWith("@")){let r=t.slice(1);return r==="-"?Pe():xt(r,"utf-8")}return t}if(!(e==="GET"||e==="DELETE")&&!process.stdin.isTTY)return Pe()}async function Pe(){let t=[];for await(let e of process.stdin)t.push(Buffer.isBuffer(e)?e:Buffer.from(e));return Buffer.concat(t).toString("utf-8")}var Ue=/#\/components\/schemas\/([A-Za-z0-9_]+)/g;function It(t,e){let r=new Set,o=[e];for(;o.length;){let n=o.pop();if(r.has(n)||!(n in t))continue;r.add(n);let i=JSON.stringify(t[n]),c;for(Ue.lastIndex=0;c=Ue.exec(i);)o.push(c[1])}return r}function Rt(t){let e=t?.paths??{};for(let r of Object.values(e))if(r?.put?.operationId==="putComposition"){let o=r.put?.requestBody?.content?.["application/json"]?.schema?.$ref,n=typeof o=="string"?o.match(/#\/components\/schemas\/(.+)$/):null;if(n)return n[1]}return null}async function Me(t,e){t!=="composition"&&(console.error(`Unknown schema '${t}'. Supported: composition.`),l(m.UserError)),X({baseUrl:q(e.target)});let r;try{r=await C("/v1/openapi.json",{method:"GET"})}catch(p){g(e,"schema",p,m.NetworkError),l(m.NetworkError)}if(r.status>=400){let p=T(r.status);g(e,"schema",new Error(`Failed to fetch /v1/openapi.json (${r.status})`),p),l(p)}let o=r.body,n=o?.components?.schemas??{},i=Rt(o);(!i||!(i in n))&&(g(e,"schema",new Error("Could not locate the composition schema in openapi.json (putComposition body ref not found)."),m.ApiServerError),l(m.ApiServerError));let c=It(n,i);c.delete(i);let d={};for(let p of[...c].sort())d[p]=n[p];let f={name:i,schema:n[i],referenced:d};v(e,f,()=>{console.log(JSON.stringify(f,null,2))})}import{writeFileSync as Tt}from"fs";import{resolve as Ot}from"path";var Fe=1800*1e3,Pt=5e3;async function Y(t,e){$(e.projectId,"projectId",e),$(e.suggestionId,"suggestionId",e),w(e.target),u(e,"export_prepare",{projectId:e.projectId,suggestionId:e.suggestionId,format:t,target:e.target});let r=x(`Queuing ${t} export\u2026`,e.json),o;try{o=(await(t==="fcpxml"?L.createFcpxmlExport:L.createPremiereExport)(e.projectId,{clipSuggestionId:e.suggestionId})).id,r.succeed(`Export queued: ${o}`)}catch(f){r.fail(`Failed: ${j(f)}`),g(e,"export_prepare",f,y(f)),l(y(f))}if(u(e,"export_queued",{exportId:o,projectId:e.projectId,format:t}),!e.wait){_(`Poll status with: cueframe api GET /v1/projects/${e.projectId}/exports/${o}`,e);return}let n=await At(e.projectId,o,e);n.kind==="failed"&&(g(e,"export_progress",Object.assign(new Error(n.message),n.error?{details:n.error}:{}),m.ApiServerError),e.json||console.error(`Export failed: ${n.message}`),l(m.ApiServerError)),n.outputUrl||(g(e,"export_complete",new Error("Export reported complete but no outputUrl was returned."),m.ApiServerError),e.json||console.error(`Export reported complete but no outputUrl was returned.
31
+ Export ID: ${o}
32
+ Re-fetch via GET /v1/projects/${e.projectId}/exports/${o}.`),l(m.ApiServerError));let c=e.out??`./export-${o.slice(0,8)}.zip`,d=await Ut(n.outputUrl,c,e.json);u(e,"export_complete",{exportId:o,projectId:e.projectId,format:t,outputUrl:n.outputUrl,outputExpiresAt:n.outputExpiresAt,outputSizeBytes:n.outputSizeBytes,trimCacheHit:n.trimCacheHit,outputPath:d})}async function At(t,e,r){let o=Date.now()+(r.timeoutMs??Fe),n=r.pollIntervalMs??Pt,i=Date.now(),c=x("Exporting\u2026",r.json);try{for(;Date.now()<o;){let d;try{d=await L.getExport(t,e)}catch(s){if(s instanceof S&&s.status>=500){await De(n);continue}throw c.fail(`Failed to poll: ${j(s)}`),s}let f=Math.round((Date.now()-i)/1e3),p=Math.round((d.progress??0)*100);if(c.text=`Exporting\u2026 ${d.status}${p>0?` \xB7 ${p}%`:""} (${f}s)`,u(r,"export_progress",{exportId:e,status:d.status,progress:d.progress,elapsedSec:f}),d.status==="complete")return c.succeed(`Export complete (${f}s)`),{kind:"complete",outputUrl:d.outputUrl??null,outputExpiresAt:d.outputExpiresAt??null,outputSizeBytes:d.outputSizeBytes??null,trimCacheHit:d.trimCacheHit??null};if(d.status==="error"||d.status==="cancelled"){let s=d.error?.message??`Export ${d.status}`;return c.fail(s),{kind:"failed",message:s,error:d.error?{phase:d.error.phase,retryable:!!d.error.retryable}:null}}await De(n)}return c.fail(`Timed out waiting for export (${Math.round((Date.now()-i)/1e3)}s)`),{kind:"failed",message:`Timed out after ${Math.round((r.timeoutMs??Fe)/1e3)}s`,error:null}}catch(d){g(r,"export_progress",d,y(d)),l(y(d))}}async function Ut(t,e,r){let o=Ot(e),n=x(`Downloading \u2192 ${o}\u2026`,r);try{let i=await fetch(t);if(!i.ok)throw new Error(`HTTP ${i.status}`);let c=Buffer.from(await i.arrayBuffer());return Tt(o,c),n.succeed(`Saved: ${o} (${(c.length/1024/1024).toFixed(1)} MB)`),o}catch(i){n.fail(`Download failed: ${j(i)}`),r||console.error(`
33
+ Export URL (7-day TTL): ${t}`),l(m.NetworkError)}}function De(t){return new Promise(e=>setTimeout(e,t))}async function Ne(t){w(t.target),u(t,"sessions_list_prepare",{});try{let e=await U.listSessions();u(t,"sessions_list_complete",{count:e.data.length}),v(t,e,()=>{if(e.data.length===0){console.log("No sessions yet. Create one with `cueframe sessions create --name <n> --files <mediaId:role> \u2026`");return}for(let r of e.data){let o=r.muxStatus??"pending";console.log(`${r.id} ${r.fileCount} files mux:${o} ${r.name}`)}e.pagination.hasMore&&console.log(`# more \u2014 pass --after ${e.pagination.nextCursor} (raw via \`cueframe api\`)`)})}catch(e){g(t,"sessions_list",e,y(e)),t.json||console.error(`\u2716 sessions list: ${j(e)}`),l(y(e))}}async function Le(t,e){$(t,"sessionId",e),w(e.target),u(e,"sessions_get_prepare",{sessionId:t});try{let r=await U.getSession(t);u(e,"sessions_get_complete",{sessionId:t}),v(e,r,()=>{console.log(JSON.stringify(r,null,2))})}catch(r){g(e,"sessions_get",r,y(r)),e.json||console.error(`\u2716 sessions get: ${j(r)}`),l(y(r))}}async function qe(t){t.name||(console.error("--name is required"),l(m.UserError)),(!t.files||t.files.length===0)&&(console.error("At least one --file <mediaId:role> is required"),l(m.UserError));let e=t.files.map((r,o)=>{let n=r.indexOf(":");return n===-1&&(console.error(`--file[${o}] must be <mediaId>:<role>. Got: ${r}`),l(m.UserError)),{mediaId:r.slice(0,n).trim(),role:r.slice(n+1).trim()}});w(t.target),u(t,"sessions_create_prepare",{name:t.name,fileCount:e.length});try{let r=await U.createSession({name:t.name,files:e});u(t,"sessions_create_complete",{sessionId:r.id}),v(t,r,()=>{console.log(`Session created: ${r.id}`),console.log(JSON.stringify(r,null,2))})}catch(r){g(t,"sessions_create",r,y(r)),t.json||console.error(`\u2716 sessions create: ${j(r)}`),l(y(r))}}async function Je(t,e){$(t,"sessionId",e),w(e.target),u(e,"sessions_delete_prepare",{sessionId:t});try{await U.deleteSession(t),u(e,"sessions_delete_complete",{sessionId:t}),v(e,{ok:!0,sessionId:t},()=>{console.log(`Session ${t} deleted.`)})}catch(r){g(e,"sessions_delete",r,y(r)),e.json||console.error(`\u2716 sessions delete: ${j(r)}`),l(y(r))}}function We(t){let e=t.commands.map(We);return{name:t.name(),description:t.description(),arguments:t.registeredArguments.map(r=>({name:r.name(),required:r.required,description:r.description})),options:t.options.map(r=>({flags:r.flags,description:r.description,...r.defaultValue!==void 0?{default:r.defaultValue}:{}})),...e.length>0?{subcommands:e}:{}}}function Be(t,e){let r=t.commands.map(We);if(e.json){u(e,"describe_complete",{name:t.name(),version:t.version()??null,commands:r});return}console.log(`${t.name()} ${t.version()??""}`.trim());for(let o of r){console.log(`
34
+ ${o.name} \u2014 ${o.description}`);for(let n of o.subcommands??[])console.log(` ${o.name} ${n.name} \u2014 ${n.description}`)}}function ze(t){if(!t)return;let e=parseInt(t,10);if(e===24||e===30||e===60)return e;console.error(`Invalid --fps ${t}. Allowed: 24, 30, 60.`),l(m.UserError)}function Ke(){let t=new Mt;t.name("cueframe").description("AI-assisted video editing and rendering").version("0.1.2").option("--json","Emit a structured JSON document on stdout instead of human-formatted output. Spinners are silenced.").option("--dry-run","Validate inputs and report what would happen \u2014 no API calls, no mutations. Applies to upload, analyze, project create, render.");let e=()=>t.opts();t.command("install").description("Install the cueframe-cli agent skill into ~/.claude/skills (Claude Code / Cursor / Codex).").action(()=>ae({json:e().json}));let r=t.command("keys").description("Manage API keys. `keys create` mints a scoped key \u2014 run `cueframe login` first (keys cannot mint keys).");r.command("create").description("Mint a scoped API key (cf_*). Requires a logged-in session.").requiredOption("-n, --name <name>","Key name, e.g. vaaya-staging").option("-s, --scope <scope>","Comma-separated resource:actions, e.g. media:rw,projects:rw,renders:rw (resources: media,sessions,projects,renders,webhooks; actions r/w)").option("--preset <preset>","Scope preset instead of --scope: readonly (read all) | editall (read+write all)").option("--expires <days>","Expire after N days (default: never)").action(s=>de({...s,json:e().json})),r.command("list").description("List your API keys.").action(()=>le({json:e().json})),r.command("revoke").description("Revoke an API key by id.").argument("<keyId>","Key id (from `cueframe keys list`)").action(s=>me(s,{json:e().json})),t.command("login").description("Browser-mediated sign-in (OAuth device flow). Recommended for laptops.").option("-t, --target <target>","Target environment: prod (default), dev, local").option("--url <url>","Override the API base URL (advanced)").action(s=>Re({...s,json:e().json})),t.command("auth").description("Save a static API key (cf_live_\u2026 / cf_test_\u2026). Use for CI / headless environments.").argument("<api-key>","Your CueFrame API key").option("-t, --target <target>","Target environment: prod (default), dev, local").option("--url <url>","Override the API base URL (advanced)").action((s,a)=>{J(s,a.target,a.url),e().json?console.log(JSON.stringify({ok:!0,target:a.target},null,2)):console.log(`API key saved for target "${a.target}"`)}),t.command("list").description("List projects").option("-t, --target <target>","Target environment: prod (default), dev, local").option("-n, --limit <n>","Max projects to show","10").action(s=>ue({target:s.target,limit:parseInt(s.limit),json:e().json}));let o=t.command("project").description("Project commands");if(o.command("create").description("Create a new project. With --from-suggestion, atomically build the composition from a clip suggestion in one round-trip \u2014 the project is then immediately renderable via `cueframe render <projectId>`.").requiredOption("-n, --name <name>","Project name").option("-t, --target <target>","Target environment: prod (default), dev, local").option("-a, --aspect <ratio>","Aspect ratio: 9:16, 1:1, 16:9","9:16").option("--fps <n>","Frame rate","30").option("--resolution <r>","Resolution: hd, fhd, 2k, 4k","fhd").option("--from-suggestion <sug>","Clip suggestion publicId (sug_\u2026) \u2014 atomically builds the composition after project creation so render needs no further args").option("--cheap","With --from-suggestion: use the deterministic translator (no LLM, ~100ms) instead of the AI authoring pass. Useful for tests / programmatic flows.").action(s=>xe({name:s.name,target:s.target,aspect:s.aspect,fps:ze(s.fps),resolution:s.resolution,fromSuggestion:s.fromSuggestion,cheap:s.cheap,json:e().json,dryRun:e().dryRun})),o.command("delete").description("Permanently delete a project (irreversible cleanup \u2014 removes the composition and render history).").argument("<projectId>","Project ID (from `cueframe list` or `project create`)").option("-t, --target <target>","Target environment: prod (default), dev, local").action((s,a)=>Ce(s,{target:a.target,json:e().json})),t.command("media").description("Media library cleanup commands").command("delete").description("Permanently delete a media item (irreversible cleanup \u2014 removes the source and its derived assets).").argument("<mediaId>","Media item ID (from `cueframe clips`)").option("-t, --target <target>","Target environment: prod (default), dev, local").action((s,a)=>ke(s,{target:a.target,json:e().json})),t.command("upload").description("Upload a video file and process it").argument("<file>","Path to video file").option("-t, --target <target>","Target environment: prod (default), dev, local").option("-a, --analyze","Run clip analysis after processing completes").action((s,a)=>fe(s,{target:a.target,analyze:a.analyze,json:e().json,dryRun:e().dryRun})),t.command("analyze").description("Trigger AI clip suggestion analysis for a media item").argument("<mediaItemId>","Media item ID (from `cueframe clips`)").option("-t, --target <target>","Target environment: prod (default), dev, local").option("-w, --wait","Wait for analysis to complete and show results").action((s,a)=>ne(s,{target:a.target,wait:a.wait,json:e().json,dryRun:e().dryRun})),t.command("clips").description("List AI-generated clip suggestions for a media item").argument("[mediaItemId]","Media item ID (omit to list your media library)").option("-t, --target <target>","Target environment: prod (default), dev, local").action((s,a)=>he(s,{target:a.target,json:e().json})),0)var p;let i=t.command("render").description("Render a project to MP4 (queues, watches via SSE, downloads). Renders whatever composition is saved on the project \u2014 build it first via `project create --from-suggestion` or `composition from-suggestion`.").allowExcessArguments(!1).argument("<projectId>","Project ID (from `cueframe list` or `project create`)").option("-t, --target <target>","Target environment: prod (default), dev, local").option("-a, --aspect <ratio>","Aspect ratio: 9:16, 1:1, 16:9 (defaults to project format)").option("--fps <n>","Frame rate","30").option("--resolution <r>","Resolution: hd, fhd, 2k, 4k","fhd").option("-o, --out <path>","Output file path (default: ./reel-<id>.mp4)").option("--idempotency-key <key>",'Override the auto-derived Idempotency-Key. Default: sha256("render:<projectId>:<currentCompositionETag>"). Pass a fresh UUID to force a new render row.').option("--priority <p>","Routing intent: interactive (low-latency, access-gated), standard (default), background (lowest queue weight).").option("--intent <i>","Encode-quality hint: preview (fast / low-bitrate, for editor scrubbing) or final (default, full Format).").action((s,a)=>Ie(s,{target:a.target,aspect:a.aspect,fps:ze(a.fps),resolution:a.resolution,out:a.out,json:e().json,dryRun:e().dryRun,...a.idempotencyKey?{idempotencyKey:a.idempotencyKey}:{},...a.priority?{priority:a.priority}:{},...a.intent?{intent:a.intent}:{}}));t.command("api").description("Raw API call against the v1 surface (escape hatch \u2014 like `gh api`).").argument("<method>","HTTP method: GET, POST, PUT, PATCH, DELETE").argument("<path>","API path starting with /v1, e.g. /v1/projects").option("-t, --target <target>","Target environment: prod (default), dev, local").option("-b, --body <body>","Request body (inline JSON, @file.json to read from disk, @- for stdin)").option("-H, --header <header...>",'Additional header (repeatable), e.g. -H "If-Match: \\"abc\\""').option("-i, --include","Include response status line + headers in stdout (curl-style). Lets agents read ETag etc.").action((s,a,h)=>Ae(s,a,{target:h.target,body:h.body,header:h.header,include:h.include,json:e().json})),t.command("schema").description("Print the live composition wire schema from the server (/v1/openapi.json) \u2014 discover the contract instead of guessing.").argument("[resource]","Schema to print (currently: composition)","composition").option("-t, --target <target>","Target environment: prod (default), dev, local").action((s,a)=>Me(s,{target:a.target,json:e().json}));let c=t.command("composition").description("Read/write a project's composition. ETag is threaded automatically.");c.command("validate").description("Validate a composition body against the live schema WITHOUT saving or rendering. Returns per-field errors \u2014 author safely before paying for a render.").requiredOption("-b, --body <body>","Composition JSON (inline, @file.json, or @- for stdin)").option("-t, --target <target>","Target environment: prod (default), dev, local").action(s=>be({target:s.target,body:s.body,json:e().json})),c.command("get").description("Fetch the project's composition and ETag").argument("<projectId>","Project ID").option("-t, --target <target>","Target environment: prod (default), dev, local").action((s,a)=>ye(s,{target:a.target,json:e().json})),c.command("put").description("Replace the composition. GETs ETag first for safe concurrent writes.").argument("<projectId>","Project ID").option("-t, --target <target>","Target environment: prod (default), dev, local").requiredOption("-b, --body <body>","Composition JSON (inline, @file.json, or @- for stdin)").option("--if-match <etag>","Use this ETag (skip the GET round-trip)").option("--force","Use If-Match: * (unconditional overwrite \u2014 may clobber concurrent edits)").action((s,a)=>we(s,{target:a.target,body:a.body,ifMatch:a.ifMatch,force:a.force,json:e().json})),c.command("from-suggestion").description("Build the composition from an existing clip suggestion").argument("<projectId>","Project ID").requiredOption("-s, --suggestion <suggestionId>","Clip suggestion publicId (sug_\u2026)").option("-t, --target <target>","Target environment: prod (default), dev, local").option("--format <json>",`Optional format override, e.g. '{"aspectRatio":"9:16","fps":30,"resolution":"fhd"}'`).option("--if-match <etag>","Use this ETag (skip the GET round-trip)").option("--force","Use If-Match: * (unconditional overwrite)").action((s,a)=>z(s,{target:a.target,suggestionId:a.suggestion,format:a.format,ifMatch:a.ifMatch,force:a.force,json:e().json})),i.command("watch").description("Reattach to an existing render and stream progress to terminal state").argument("<renderId>","Render ID (from `cueframe render` output)").requiredOption("-p, --project <projectId>","Project the render belongs to").option("-t, --target <target>","Target environment: prod (default), dev, local").option("--no-download","Print the MP4 URL but skip downloading the file").option("-o, --out <path>","Output file path when downloading (default: ./reel-<id>.mp4)").action((s,a,h)=>{let E=h?.parent?.opts?.()?.target??a.target;return _e(s,{target:E,project:a.project,out:a.out,download:a.download!==!1,json:e().json})});let d=t.command("export").description("Hand the timeline off to a real NLE (Final Cut Pro / Premiere Pro)");d.command("fcpxml").description("Build a Final Cut Pro export zip and (optionally) wait to download it").argument("<projectId>","Project ID").requiredOption("-s, --suggestion <suggestionId>","Clip suggestion publicId (sug_\u2026)").option("-t, --target <target>","Target environment: prod (default), dev, local").option("-o, --out <path>","Output zip path (default: ./export-<id>.zip)").option("--wait","Block until the export finishes, then download the artifact").action((s,a)=>Y("fcpxml",{target:a.target,projectId:s,suggestionId:a.suggestion,out:a.out,wait:a.wait,json:e().json})),d.command("premiere").description("Build a Premiere Pro export zip and (optionally) wait to download it").argument("<projectId>","Project ID").requiredOption("-s, --suggestion <suggestionId>","Clip suggestion publicId (sug_\u2026)").option("-t, --target <target>","Target environment: prod (default), dev, local").option("-o, --out <path>","Output zip path (default: ./export-<id>.zip)").option("--wait","Block until the export finishes, then download the artifact").action((s,a)=>Y("premiere",{target:a.target,projectId:s,suggestionId:a.suggestion,out:a.out,wait:a.wait,json:e().json}));let f=t.command("sessions").description("Multi-file ingest sessions (camera A/B/screen \u2192 muxed composition)");f.command("list").description("List sessions in this org").option("-t, --target <target>","Target environment: prod (default), dev, local").action(s=>Ne({target:s.target,json:e().json})),f.command("get").description("Get a single session by ID").argument("<sessionId>","Session ID").option("-t, --target <target>","Target environment: prod (default), dev, local").action((s,a)=>Le(s,{target:a.target,json:e().json})),f.command("create").description("Create a session from one or more media files").requiredOption("-n, --name <name>","Session name").requiredOption("-f, --file <entry...>","Each entry is <mediaId>:<role> (repeatable)").option("-t, --target <target>","Target environment: prod (default), dev, local").action(s=>qe({target:s.target,name:s.name,files:s.file,json:e().json})),f.command("delete").description("Delete a session (server checks for compositions referencing it)").argument("<sessionId>","Session ID").option("-t, --target <target>","Target environment: prod (default), dev, local").action((s,a)=>Je(s,{target:a.target,json:e().json})),t.command("describe").description("Print the CLI command tree as structured JSON (agent introspection)").action(()=>Be(t,{json:e().json})),t.parse()}Ke();
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "cueframe",
3
+ "version": "0.1.0",
4
+ "description": "Agent-native CLI for CueFrame — author compositions and render video from the terminal (early-access preview).",
5
+ "keywords": [
6
+ "cueframe",
7
+ "video",
8
+ "render",
9
+ "composition",
10
+ "cli",
11
+ "agent",
12
+ "ffmpeg",
13
+ "remotion"
14
+ ],
15
+ "homepage": "https://cueframe.ai",
16
+ "author": "CueFrame",
17
+ "license": "UNLICENSED",
18
+ "type": "module",
19
+ "bin": {
20
+ "cueframe": "dist/cueframe.js"
21
+ },
22
+ "main": "dist/cueframe.js",
23
+ "files": [
24
+ "dist",
25
+ "skills",
26
+ "README.md"
27
+ ],
28
+ "engines": {
29
+ "node": ">=18"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "dependencies": {
35
+ "better-auth": "1.5.3",
36
+ "commander": "^13.1.0",
37
+ "open": "^10.1.0",
38
+ "ora": "^8.2.0",
39
+ "pino": "^10.3.1"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^22.15.0",
43
+ "tsup": "^8.5.1",
44
+ "typescript": "^5.8.3",
45
+ "vitest": "^4.1.0",
46
+ "@cueframe/cloud-import": "0.1.0",
47
+ "@cueframe/sdk": "0.0.0"
48
+ },
49
+ "scripts": {
50
+ "build": "tsup",
51
+ "check": "tsc --noEmit",
52
+ "test": "vitest run"
53
+ }
54
+ }
@@ -0,0 +1,200 @@
1
+ ---
2
+ name: cueframe-cli
3
+ description: Use when editing, clipping, or rendering video via the CueFrame CLI — e.g. "make a 9:16 short from this podcast", "pull the best moments out of this MP4", "render a clip", or any task that calls the `cueframe` binary. Also use when comparing CueFrame against direct-ffmpeg approaches.
4
+ ---
5
+
6
+ # CueFrame CLI
7
+
8
+ ## Overview
9
+
10
+ The `cueframe` CLI drives the CueFrame platform from the terminal. Optimized for agents — every command supports `--json` (NDJSON event stream) and the binary self-describes via `cueframe describe --json`.
11
+
12
+ **The framework is three verbs:** `upload` (register media) → `composition put` (author the timeline) → `render` (→ MP4). The `Composition` is the contract — render takes the saved composition, nothing else. Everything domain-specific (AI clip suggestions, FCPXML/Premiere export) is a **use-case on-ramp** layered on top, not part of the core.
13
+
14
+ **Core principle — discover, don't guess.** Never author a composition from the
15
+ examples below alone. The live contract is `cueframe schema composition` (prints
16
+ the real wire schema the server enforces); confirm any draft with
17
+ `cueframe composition validate -b @file.json` BEFORE you render. Validation is
18
+ free and instant; a render is neither. The examples here are orientation — the
19
+ schema is the source of truth.
20
+
21
+ **Core principle:** Drive everything through `cueframe`. Never `curl` the v1 API around it — that invalidates the work and hides CLI bugs.
22
+
23
+ ## Framework workflow (upload → compose → render)
24
+
25
+ The general path for any agent. Author the timeline yourself and render it — no
26
+ clip suggestion required.
27
+
28
+ ```bash
29
+ # 0. Always use --json for agent-driven runs (NDJSON to stdout, no spinners).
30
+ # This build connects to a fixed environment — no target flag needed.
31
+
32
+ # 1. Register media (one or many — mixed video / image / audio).
33
+ cueframe upload "/path/to/source.mp4" --json # → mediaItemId (process_complete)
34
+
35
+ # 2. Create a project (sets aspect/format).
36
+ cueframe project create -n "promo" -a 9:16 --json # → projectId
37
+
38
+ # 3. Author the composition: tracks of clips (per-clip reframe/trim), optional
39
+ # captions (captions.segments), brandKitId. The composition is the SSOT.
40
+ # Discover the live schema, then validate the draft BEFORE saving/rendering:
41
+ cueframe schema composition # → live wire schema
42
+ cueframe composition validate -b @composition.json # → ✓ valid | per-field errors (no render)
43
+ cueframe composition put <projectId> -b @composition.json # ETag auto
44
+
45
+ # 4. Render the saved composition to MP4. SSE-watches + downloads when done.
46
+ cueframe render <projectId> -o ./out.mp4 --json
47
+ # → render_queued → render_progress* → render_complete
48
+ #
49
+ # `render` takes ONLY a projectId and renders the SAVED composition (tracks +
50
+ # captions). A clip suggestion is NOT required. Same project → same MP4 (idempotent).
51
+ # Captions: render reads composition.captions — put caption words there to caption
52
+ # a hand-authored composition; if absent, the clip is rendered without captions.
53
+
54
+ # Reattach to an in-flight render later (e.g. after disconnect):
55
+ cueframe render watch <renderId> -p <projectId> -o ./out.mp4 --json
56
+ ```
57
+
58
+ ## Anatomy of a composition.json
59
+
60
+ This section orients you to the shape; the authoritative contract is
61
+ `cueframe schema composition` (the live wire schema) — run it when a field is
62
+ unclear, and `cueframe composition validate -b @file.json` to check a draft.
63
+
64
+ `composition put` takes the **bare** `Composition` object (no wrapper; the CLI POSTs
65
+ the `@file` verbatim and handles the ETag). Minimal valid shape:
66
+
67
+ ```json
68
+ {
69
+ "v": 1,
70
+ "format": { "aspectRatio": "9:16", "fps": 30 },
71
+ "tracks": [
72
+ { "id": "v1", "kind": "video", "contents": [
73
+ { "id": "c1", "startTime": 0, "duration": 10, "source": { "kind": "media", "mediaId": "<mediaId>" } }
74
+ ] }
75
+ ]
76
+ }
77
+ ```
78
+
79
+ - **Required**: `v: 1`, `format.aspectRatio` (`16:9` | `9:16` | `1:1` | `4:5`), `tracks`.
80
+ - Tracks hold **`contents`** (not `clips`); each clip wraps a `source`. A clip's
81
+ `source.kind` must match the track `kind`: `media` → `video`/`image`/`audio`,
82
+ `overlay` → `overlay`, `effect` → `effect`.
83
+ - `source.kind`s: `media` (`mediaId` + optional `trim`/`reframe`/`volume`), `overlay`
84
+ (`primitiveId` + `params` + optional `zPlane`), `effect` (`primitiveId` + `params`).
85
+ - Times are **seconds** (`startTime`/`duration`/`trim`, and `reframe` `startSec`/`endSec`);
86
+ caption word times are **milliseconds**. `region`/`fit` (PiP/inset) are clip-level.
87
+ - Clips on one track can't overlap — simultaneous layers go on separate tracks.
88
+ - Optional top-level: `captions` (`segments[].words[]`), `brandKitId`, `markers`.
89
+
90
+ For full per-video-type examples (auto-zoom demo, PiP, split-screen, captions,
91
+ behind-subject title), see the `cueframe-product-video` skill.
92
+
93
+ ## Render resolves expensive intents automatically — author, don't pre-bake
94
+
95
+ You author *intent* on the composition; `render` materializes the expensive
96
+ artifacts it implies. Nothing to pre-bake, no compose step required:
97
+
98
+ - An overlay with `"zPlane":"behind-subject"` → the person **matte** is baked
99
+ on-miss and the text is placed behind the speaker (shoulder-band anchor +
100
+ occlusion handled). Author it and render — it just works.
101
+ - A clip whose `reframe.focus` is `{mode:"face",faceId}` / `{mode:"active-speaker"}`
102
+ / `{mode:"all-faces"}` → **subject detection** runs on-miss so the crop tracks the
103
+ real person. (`{mode:"point"}` / `{mode:"frame-center"}` need no detection.)
104
+
105
+ During this the SSE `render_progress` phases read
106
+ `resolving → baking-matte | detecting → rendering → complete` — the first behind/face
107
+ render of a source pays a one-time bake (cached after; re-renders are fast).
108
+
109
+ ### Render error codes (fail-loud — render never ships a wrong frame silently)
110
+
111
+ | `code` | meaning | fix |
112
+ |---|---|---|
113
+ | `media_not_found` | a referenced `mediaId` doesn't exist or isn't your org's | reference media you uploaded |
114
+ | `media_not_ready` | the media has no file yet (still uploading/generating) | wait for `upload`'s `process_complete` |
115
+ | `source_dims_unknown` | a `reframe` clip's source has no width/height/fps yet | let the source finish processing before authoring a reframe |
116
+ | `content_anchor` | a clip's trim frames *different* footage than its captions show (wrong-footage) | align the trim window to the captioned moment |
117
+ | `behind_split_unsupported` | one media used under two *different* behind-subject windows | one behind window per media (split into separate clips/media) |
118
+ | `BehindMatteUnresolved` / `TrajectoryUnresolved` | the matte/detection bake produced no result for a behind/face clip | the source may have no detectable person; retry, or drop the behind/face intent |
119
+
120
+ ## Clip-suggestion on-ramp ("podcast → short clip")
121
+
122
+ ONE use-case layered on the framework: the AI clip-finder picks a moment and
123
+ **authors a composition for you** (trim + speaker framing + captions sliced from
124
+ the transcript). Use it when you want CueFrame to choose the clip; otherwise author
125
+ the composition yourself (above).
126
+
127
+ ```bash
128
+ # 1. Upload + chain the AI clip pass.
129
+ cueframe upload "/path/to/source.mp4" --analyze --json # mediaItemId + suggestions
130
+ # (or run it explicitly: cueframe analyze <mediaItemId> --wait --json)
131
+
132
+ # 2. List clip suggestions (publicId sug_…, title, startMs/endMs, score).
133
+ cueframe clips <mediaItemId> --json
134
+
135
+ # 3. Create a project AND author its composition from the chosen suggestion.
136
+ cueframe project create -n "paul-klein-shorts" -a 9:16 --from-suggestion <sug_…> --json
137
+ # (or, on an existing project: cueframe composition from-suggestion <projectId> -s <sug_…>)
138
+
139
+ # 4. Render — same framework verb as above.
140
+ cueframe render <projectId> -o ./out.mp4 --json
141
+ ```
142
+
143
+ Both flows converge on `composition put` → `render`; the on-ramp just authors the
144
+ composition from a suggestion instead of you hand-writing it.
145
+
146
+ Pipe `--json` output through `jq -c 'select(.event=="…")'` to grab specific lifecycle events.
147
+
148
+ ## Quick reference
149
+
150
+ | Need to… | Command |
151
+ |---|---|
152
+ | See every command + flags | `cueframe describe --json` |
153
+ | (Re)install/update this skill | `cueframe install` (→ `~/.claude/skills/cueframe-cli`) |
154
+ | List projects | `cueframe list -n 50` |
155
+ | List your media | `cueframe clips` (no mediaId arg) |
156
+ | Discover the live composition schema | `cueframe schema composition` |
157
+ | Validate a draft (no save, no render) | `cueframe composition validate -b @plan.json` |
158
+ | Read composition + ETag | `cueframe composition get <projectId> --json` |
159
+ | Write composition | `cueframe composition put <projectId> -b @plan.json` (ETag auto) |
160
+ | Export to Final Cut | `cueframe export fcpxml <projectId> -s <sug> --wait -o cut.zip` |
161
+ | Export to Premiere | `cueframe export premiere <projectId> -s <sug> --wait -o cut.zip` |
162
+ | Anything not wrapped | `cueframe api GET /v1/... -i` (escape hatch, like `gh api`) |
163
+ | Validate a composition without mutating | `cueframe composition validate -b @plan.json` |
164
+
165
+ ## Targets and auth
166
+
167
+ **This build connects to a fixed CueFrame environment.** The `-t` / `--target`, `--url`, and `CUEFRAME_API_URL` inputs are accepted but **ignored** — there is no environment to select. Authenticate once:
168
+
169
+ - `cueframe auth <cf_test_… key>` — save a static API key (CI / headless).
170
+ - `cueframe login` — OAuth device flow (laptops).
171
+
172
+ Creds are stored in `~/.cueframe/auth.json`; the key can also come from the `CUEFRAME_API_KEY` env var.
173
+
174
+ ## NDJSON event shape
175
+
176
+ Every `--json` line:
177
+ ```json
178
+ {"_":0,"schemaVersion":"v1","ts":1779250551614,"event":"<verb>_<phase>", ...payload}
179
+ ```
180
+ Phase lifecycle: `<verb>_prepare` → `<verb>_ready` → `<verb>_progress` (0+) → `<verb>_complete` | `error`. The `_:0` field is a pino formatter placeholder — ignore it.
181
+
182
+ Errors come as `{"event":"error", "phase":"<step>", "message":"...", "code":"...", "exitCode":N}` *and* a non-zero process exit. Read both.
183
+
184
+ ## Common mistakes
185
+
186
+ | Mistake | Reality |
187
+ |---|---|
188
+ | Reaching for `curl …/v1/...` because the CLI didn't have a command | Use `cueframe api METHOD /v1/...` — same auth, same JSON pipeline. |
189
+ | Passing `-t` / `--url` to switch environment | No-op. The target is fixed for this release; those inputs are ignored. |
190
+ | Polling for render completion with a sleep loop | `cueframe render` already SSE-watches and exits when done. Use `render watch <id>` to reattach. |
191
+ | Skipping `--json` and trying to parse human output | Human output is for humans. Agents always use `--json`. |
192
+ | Hand-editing the composition without an ETag | `composition put` GETs the ETag for you; `from-suggestion` rebuilds from a clip. Don't hand-craft unless you have to. |
193
+ | Calling `cueframe render <projectId> <suggestionId>` (old two-positional shape) | `render` takes ONLY a projectId; commander rejects the extra arg. It renders the SAVED composition — author it first via `composition put` (hand-author) or `project create --from-suggestion <sug>` (clip-finder on-ramp). |
194
+ | Assuming `render` needs a clip suggestion / that captions only come from a suggestion | No — `render` renders the saved composition directly; a suggestion is optional. Captions come from `composition.captions` (author them in the `put` JSON). The clip-suggestion on-ramp just fills those captions for you by slicing the transcript at author time. |
195
+
196
+ ## When NOT to use this skill
197
+
198
+ - Pure local video manipulation (concatenate, trim, transcode) where AI clip selection adds no value → use `ffmpeg` directly.
199
+ - Inspecting internal backend state → that has its own operator tooling, not this CLI.
200
+ - Building the CLI itself (changes to the CLI source) → that's CLI development, not CLI usage.
@@ -0,0 +1,103 @@
1
+ ---
2
+ name: cueframe-compose-loop
3
+ description: Use when an AI agent should DRIVE CueFrame to a high-quality clip itself — author a composition, look at rendered frames, score it with CueFrame's judge, fix the weakest thing, and repeat until it's good — e.g. "turn this source into a polished 9:16 clip", "compose and self-correct until it scores well". This is the agent-driven convergence loop over the CueFrame primitives in the `cueframe-cli` skill.
4
+ ---
5
+
6
+ # CueFrame — Compose convergence loop
7
+
8
+ You are the director. CueFrame gives you hands (author), eyes (capture), and a
9
+ judge (verify); you supply the taste and the iteration. The engine does NOT
10
+ auto-compose for you here — you drive the loop and decide when it's done. Drive
11
+ the primitives with the `cueframe-cli` skill; this tells you *how to converge*.
12
+
13
+ ## The loop
14
+
15
+ ```
16
+ get_context → author → capture (look) → verify (score)
17
+ ↑ │
18
+ └──── fix the WORST criterion ──┘ → stop → render
19
+ ```
20
+
21
+ Run it on a warm **session** (one boot, many fast renders), not one-shot renders.
22
+
23
+ ## 0. Context — always first
24
+
25
+ `GET /v1/media/:id/context` (`cueframe_api_getMediaContext`) returns what you need
26
+ to author well for a source:
27
+
28
+ - **faces** — the subject roster per source window: `faceId` (face-0 = largest/
29
+ primary speaker), normalized `bbox`, `speakingShare` (0–1). Use these to aim
30
+ reframe/crop intents at the right subject. `faces.status: "not_detected"` means
31
+ no detection has run yet — author without face-targeted reframe, or trigger a
32
+ pass first; never invent face ids.
33
+ - **transcript** — utterances with per-word `start`/`end` (seconds). Use for
34
+ caption timing and for placing overlays on the right beat.
35
+
36
+ ## 1. Author
37
+
38
+ Build/modify the working composition with the op vocabulary —
39
+ `cueframe_api_applyCompositionOp` (one op) or `putComposition` (whole). Op keys
40
+ are `.strict()`: a typo'd key is rejected, not silently dropped, so read the
41
+ error and fix the key. Reference faces from get_context in your reframe segments;
42
+ time captions/overlays from the transcript word timings.
43
+
44
+ ## 2. Open a session
45
+
46
+ `POST /v1/projects/:id/compose-session` → `{ sessionId }`. Boots a warm render box
47
+ (~60–120s the first time). Reuse it for every capture/verify in the loop; close
48
+ it (`DELETE …/:sessionId`) when done — it also self-reaps at its TTL.
49
+
50
+ ## 3. Capture — look at it
51
+
52
+ `POST /v1/projects/:id/compose-session/:sessionId/render` with `{ composition,
53
+ timestamps }` → stills, each with a presigned `url` you can actually view. Look at
54
+ the beats you're unsure about (a cut, an overlay entrance, the hook). Stills are
55
+ real render output — what you see is what ships.
56
+
57
+ ## 4. Verify — score it
58
+
59
+ `POST /v1/projects/:id/compose-session/:sessionId/verify` with `{ composition,
60
+ editorialIntent?, brandContext? }`. CueFrame's judge samples the meaningful beats,
61
+ renders them, and returns:
62
+
63
+ ```
64
+ { perCriterion: { editorial, spatial, brand, caption } each { score 0–10, critique },
65
+ composite, // weighted overall
66
+ critique, // worst-first, leads with the criterion to fix
67
+ beats }
68
+ ```
69
+
70
+ The four criteria:
71
+
72
+ | criterion | what it grades |
73
+ |---|---|
74
+ | **editorial** | does the framing/cut serve the stated intent (the hook, the point)? |
75
+ | **spatial** | subject framing — centered/tight enough, not lost in dead space, not occluded by captions |
76
+ | **brand** | on-brand color/typography/voice; cohesive look |
77
+ | **caption** | caption legibility, timing, placement (or `no_captions` when absent) |
78
+
79
+ You get **scores and the critique, never the rubric** — the taste is CueFrame's.
80
+ Pass `editorialIntent` (your brief/hook) so editorial is graded against what you
81
+ meant.
82
+
83
+ ## 5. Fix the worst — then re-verify
84
+
85
+ Read `critique` — it names the lowest criterion first and says why. Fix **that one
86
+ thing** (re-author the relevant ops), then verify again. One criterion per pass:
87
+ chasing all four at once thrashes. Example: editorial 4 "two subjects off-center
88
+ vs centered intent" → tighten the reframe onto face-0 → re-verify.
89
+
90
+ ## 6. Stop — then render
91
+
92
+ Stop when **composite ≥ ~7.5** OR a pass doesn't improve the composite (two
93
+ no-improvement passes = diminishing returns; ship the best so far). Then render
94
+ the final: `createRender` (see `cueframe-cli`) → MP4.
95
+
96
+ ## Notes
97
+
98
+ - **Whose tokens:** you run this loop on your own model/tokens; CueFrame bills the
99
+ infra (capture/render) + the judge. That's the point — you keep the orchestration.
100
+ - **Don't re-derive the judge or beat sampling** — verify owns both. Your job is
101
+ author → read scores → fix worst → repeat.
102
+ - **Budget the loop:** 3–5 verify passes is plenty for most clips. If composite
103
+ won't climb, the limiter is usually the source or the intent, not more passes.