@@ -88,6 +88,14 @@ const MEDIA_GENERATE_BOOLEAN_FLAGS = new Set([
8888 'h' ,
8989 'loop' ,
9090] ) ;
91+ const MEDIA_SCAFFOLD_STRING_FLAGS = new Set ( [
92+ 'project' ,
93+ 'workspace' ,
94+ 'workspace-member' ,
95+ 'composition-dir' ,
96+ 'daemon-url' ,
97+ ] ) ;
98+ const MEDIA_SCAFFOLD_BOOLEAN_FLAGS = new Set ( [ 'help' , 'h' , 'json' ] ) ;
9199
92100const MCP_STRING_FLAGS = new Set ( [
93101 'daemon-url' ,
@@ -936,6 +944,10 @@ function printRootHelp() {
936944 Designed to be invoked by a code agent - picks up OD_DAEMON_URL
937945 and OD_PROJECT_ID from the env that the daemon injected on spawn.
938946
947+ od media scaffold --composition-dir .hyperframes-cache/<id>
948+ Create a deterministic HyperFrames composition without npx or global
949+ skill installation, before dispatching it through media generate.
950+
939951 od mcp [--daemon-url <url>]
940952 Run a stdio MCP server that proxies project tool calls to a
941953 running Open Design daemon. Wire it into a coding agent
@@ -954,8 +966,8 @@ What the daemon does:
954966 * scans PATH for installed code-agent CLIs (claude, codex, devin, opencode, cursor-agent, ...)
955967 * serves the chat UI at http://<host>:<port>
956968 * proxies messages (text + images) to the selected agent via child-process spawn
957- * exposes /api/projects/:id/ media/generate — the unified image/video/audio
958- dispatcher that the agent calls via \`od media generate\`.` ) ;
969+ * exposes project-scoped media scaffold /generate APIs — the unified path
970+ that the agent calls via \`od media scaffold\` and \`od media generate\`.` ) ;
959971}
960972
961973// ---------------------------------------------------------------------------
@@ -1626,7 +1638,7 @@ async function runMedia(args) {
16261638 printMediaHelp ( ) ;
16271639 return ;
16281640 }
1629- if ( sub !== 'generate' && sub !== 'wait' ) {
1641+ if ( sub !== 'generate' && sub !== 'wait' && sub !== 'scaffold' ) {
16301642 console . error ( `unknown subcommand: od media ${ sub } ` ) ;
16311643 printMediaHelp ( ) ;
16321644 process . exit ( 1 ) ;
@@ -1635,9 +1647,66 @@ async function runMedia(args) {
16351647 const idx = args . indexOf ( sub ) ;
16361648 const subArgs = [ ...args . slice ( 0 , idx ) , ...args . slice ( idx + 1 ) ] ;
16371649 if ( sub === 'wait' ) return runMediaWait ( subArgs ) ;
1650+ if ( sub === 'scaffold' ) return runMediaScaffold ( subArgs ) ;
16381651 return runMediaGenerate ( subArgs ) ;
16391652}
16401653
1654+ async function runMediaScaffold ( rawArgs ) {
1655+ let flags ;
1656+ try {
1657+ flags = parseFlags ( rawArgs , {
1658+ string : MEDIA_SCAFFOLD_STRING_FLAGS ,
1659+ boolean : MEDIA_SCAFFOLD_BOOLEAN_FLAGS ,
1660+ } ) ;
1661+ } catch ( err ) {
1662+ console . error ( err . message ) ;
1663+ printMediaHelp ( ) ;
1664+ process . exit ( 2 ) ;
1665+ }
1666+ if ( flags . help || flags . h ) {
1667+ printMediaHelp ( ) ;
1668+ return ;
1669+ }
1670+
1671+ const daemonUrl = await cliDaemonUrl ( flags ) ;
1672+ const projectId = flags . project || process . env . OD_PROJECT_ID ;
1673+ const token = process . env . OD_TOOL_TOKEN ;
1674+ if ( ! projectId && ! token ) {
1675+ console . error ( 'project id required. Pass --project <id> or set OD_PROJECT_ID.' ) ;
1676+ process . exit ( 2 ) ;
1677+ }
1678+ const compositionDir = flags [ 'composition-dir' ] ;
1679+ if ( ! compositionDir ) {
1680+ console . error ( '--composition-dir required (expected .hyperframes-cache/<id>)' ) ;
1681+ process . exit ( 2 ) ;
1682+ }
1683+
1684+ const url = token
1685+ ? `${ daemonUrl . replace ( / \/ $ / , '' ) } /api/tools/media/hyperframes/scaffold`
1686+ : `${ daemonUrl . replace ( / \/ $ / , '' ) } /api/projects/${ encodeURIComponent ( projectId ) } /media/hyperframes/scaffold` ;
1687+ let resp ;
1688+ try {
1689+ resp = await fetch ( url , {
1690+ method : 'POST' ,
1691+ headers : {
1692+ 'content-type' : 'application/json' ,
1693+ ...( token ? { authorization : `Bearer ${ token } ` } : { } ) ,
1694+ ...( token ? { } : workspaceHeadersFromExplicitFlags ( flags ) ?? { } ) ,
1695+ } ,
1696+ body : JSON . stringify ( { compositionDir } ) ,
1697+ } ) ;
1698+ } catch ( err ) {
1699+ surfaceFetchError ( err , daemonUrl ) ;
1700+ process . exit ( 3 ) ;
1701+ }
1702+ if ( ! resp . ok ) {
1703+ const responseText = await resp . text ( ) ;
1704+ console . error ( `daemon ${ resp . status } : ${ responseText } ` ) ;
1705+ process . exit ( 4 ) ;
1706+ }
1707+ process . stdout . write ( `${ JSON . stringify ( await resp . json ( ) ) } \n` ) ;
1708+ }
1709+
16411710async function runMediaGenerate ( rawArgs ) {
16421711 let flags ;
16431712 try {
@@ -2013,10 +2082,17 @@ async function cliDaemonBaseUrl(flags) {
20132082}
20142083
20152084function printMediaHelp ( ) {
2016- console . log ( `Usage: od media generate --surface <image|video|audio> --model <id> [opts]
2085+ console . log ( `Usage: od media scaffold --composition-dir .hyperframes-cache/<id> [opts]
2086+ od media generate --surface <image|video|audio> --model <id> [opts]
20172087 "$OD_NODE_BIN" "$OD_BIN" media generate --surface <image|video|audio> --model <id> [opts]
20182088
2019- Required:
2089+ Scaffold:
2090+ Creates hyperframes.json, meta.json, and index.html without running
2091+ HyperFrames init or installing global skills. The target must be new and
2092+ live directly under .hyperframes-cache.
2093+ --json is accepted for consistency; scaffold output is always one JSON line.
2094+
2095+ Generate required:
20202096 --surface image | video | audio
20212097 --model Model id from /api/media/models (e.g. gpt-image-2, seedance-2, suno-v5).
20222098 --project Project id. Auto-resolved from OD_PROJECT_ID when invoked by the daemon.
@@ -2044,8 +2120,9 @@ Common options:
20442120 --audio-kind music|speech|sfx
20452121 --composition-dir <path> hyperframes-html only — project-relative path
20462122 to the dir containing hyperframes.json /
2047- meta.json / index.html. The daemon runs
2048- \`npx hyperframes render\` against it.
2123+ meta.json / index.html. Use \`media scaffold\` to
2124+ create it; the daemon renders it with its pinned
2125+ HyperFrames runtime.
20492126 --image <path> Project-relative reference image; repeat up to 5
20502127 times for Vela image editing or video references.
20512128 The first video image is the first frame; the rest
0 commit comments