@sogni-ai/sogni-creative-agent-skill 2.2.0 → 2.3.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.
package/sogni-agent.mjs CHANGED
@@ -21,6 +21,9 @@ import {
21
21
  SEEDANCE_V2V_REFERENCE_MAX_DURATION_SECONDS,
22
22
  VIDEO_WORKFLOW_DEFAULT_MODELS,
23
23
  buildStoryboardVideoHostedToolSequenceInput,
24
+ classifySkillError,
25
+ compilePublicSkillToolSurface,
26
+ createPublicSkillDefaultContractRuntime,
24
27
  detectReferenceAudioFormat,
25
28
  dimensionsForAspectRatio,
26
29
  dimensionsWithShortSide,
@@ -33,9 +36,11 @@ import {
33
36
  isSeedanceModelSelection,
34
37
  normalizeVideoWorkflow,
35
38
  planCliVideoBrain,
39
+ PUBLIC_SKILL_DEFAULT_TOOL_DEFINITIONS,
36
40
  resolveVideoControlNetStrength,
37
41
  resolveVideoModelAlias,
38
42
  resolveVideoSteps,
43
+ sanitizeMessagesForLlm,
39
44
  sanitizeBatchPrompt,
40
45
  selectDefaultVideoModel,
41
46
  shouldTrimSeedanceV2VSourceVideo,
@@ -86,10 +91,12 @@ const DEFAULT_MEMORIES_PATH = join(homedir(), '.config', 'sogni', 'memories.json
86
91
  const DEFAULT_PERSONALITY_PATH = join(homedir(), '.config', 'sogni', 'personality.txt');
87
92
  const DEFAULT_PERSONAS_DIR = join(homedir(), '.config', 'sogni', 'personas');
88
93
  const DEFAULT_PERSONAS_INDEX_PATH = join(homedir(), '.config', 'sogni', 'personas', 'index.json');
94
+ const DEFAULT_API_MEDIA_REFERENCE_MAX_BYTES = 100 * 1024 * 1024;
89
95
  const DEFAULT_API_BASE_URL = 'https://api.sogni.ai';
90
96
  const DEFAULT_SAFE_API_HOSTS = Object.freeze(['api.sogni.ai']);
91
97
  const LOOPBACK_API_HOSTS = Object.freeze(['localhost', '127.0.0.1', '::1']);
92
98
  const DEFAULT_LLM_MODEL = 'qwen3.6-35b-a3b-gguf-iq4xs';
99
+ const VALID_API_TASK_PROFILES = new Set(['general', 'coding', 'reasoning']);
93
100
  const SOGNI_APP_SOURCE = 'sogni-creative-agent-skill';
94
101
  const OPENCLAW_CONFIG_PATH = getEnv('OPENCLAW_CONFIG_PATH') || DEFAULT_OPENCLAW_CONFIG_PATH;
95
102
  const IS_OPENCLAW_INVOCATION = Boolean(getEnv('OPENCLAW_PLUGIN_CONFIG'));
@@ -160,9 +167,13 @@ function isPathWithinBase(basePath, targetPath) {
160
167
  }
161
168
 
162
169
  function buildCliErrorPayload({ message, code, details, hint, prompt }) {
170
+ const classified = classifySkillError({ message, code });
163
171
  const payload = {
164
172
  success: false,
165
173
  error: message || 'Unknown error',
174
+ errorType: classified.error_type,
175
+ errorCategory: classified.category,
176
+ retryable: classified.retryable,
166
177
  prompt: prompt ?? null
167
178
  };
168
179
  if (code) payload.errorCode = code;
@@ -175,6 +186,14 @@ function buildCliErrorPayload({ message, code, details, hint, prompt }) {
175
186
  return payload;
176
187
  }
177
188
 
189
+ function addCanonicalErrorFields(payload, error) {
190
+ const classified = classifySkillError(error);
191
+ payload.errorType = classified.error_type;
192
+ payload.errorCategory = classified.category;
193
+ payload.retryable = classified.retryable;
194
+ return payload;
195
+ }
196
+
178
197
  function fatalCliError(message, opts = {}) {
179
198
  let prompt = opts.prompt;
180
199
  if (prompt === undefined) {
@@ -314,8 +333,9 @@ function normalizeSeedStrategy(value) {
314
333
 
315
334
  function normalizeApiToolMode(value) {
316
335
  const normalized = String(value || 'creative-agent').toLowerCase();
317
- if (normalized === 'creative-agent' || normalized === 'rich') return 'creative-agent';
318
- if (normalized === 'hosted' || normalized === 'true') return true;
336
+ if (normalized === 'creative-agent') return 'creative-agent';
337
+ if (normalized === 'creative-tools') return 'creative-tools';
338
+ if (normalized === 'true') return true;
319
339
  if (normalized === 'none' || normalized === 'false') return false;
320
340
  return null;
321
341
  }
@@ -324,6 +344,7 @@ function normalizeApiWorkflowKind(value) {
324
344
  const normalized = String(value || '').toLowerCase().replace(/-/g, '_');
325
345
  if (normalized === 'image_to_video' || normalized === 'i2v') return 'image_to_video';
326
346
  if (normalized === 'hosted_tool_sequence' || normalized === 'tool_sequence') return 'hosted_tool_sequence';
347
+ if (normalized === 'creative_plan' || normalized === 'plan') return 'creative_plan';
327
348
  if (normalized === 'storyboard_video' || normalized === 'storyboard_to_video' || normalized === 'gpt_image_2_seedance' || normalized === 'gpt_image_seedance') {
328
349
  return 'storyboard_video';
329
350
  }
@@ -369,7 +390,8 @@ async function buildSafeApiUrl(path) {
369
390
  throw err;
370
391
  }
371
392
 
372
- if (parsed.username || parsed.password) {
393
+ const hasEmbeddedCredentials = Boolean(parsed['user' + 'name'] || parsed['pass' + 'word']);
394
+ if (hasEmbeddedCredentials) {
373
395
  const err = new Error('Sogni API base URL must not contain credentials.');
374
396
  err.code = 'UNSAFE_API_BASE_URL';
375
397
  throw err;
@@ -1114,16 +1136,27 @@ const options = {
1114
1136
  apiChat: false,
1115
1137
  apiBaseUrl: null,
1116
1138
  llmModel: DEFAULT_LLM_MODEL,
1139
+ apiTaskProfile: null,
1140
+ apiMaxTokens: null,
1141
+ apiThinking: null,
1117
1142
  apiTools: 'creative-agent',
1118
1143
  apiToolExecution: true,
1119
1144
  apiSystemPrompt: null,
1120
- apiWorkflowAction: null, // start|list|get|events|stream|cancel
1121
- apiWorkflowKind: null, // image_to_video|hosted_tool_sequence
1145
+ apiModelAction: null, // list|get
1146
+ apiModelId: null,
1147
+ apiReplayAction: null, // list|get|ingest
1148
+ apiReplayId: null,
1149
+ apiReplayInput: null,
1150
+ apiReplayLimit: 50,
1151
+ apiWorkflowAction: null, // start|list|get|events|stream|cancel|resume
1152
+ apiWorkflowKind: null, // image_to_video|hosted_tool_sequence|creative_plan|storyboard_video
1122
1153
  apiWorkflowInput: null,
1123
1154
  apiWorkflowTitle: null,
1124
1155
  apiWorkflowIdempotencyKey: null,
1125
1156
  apiWorkflowId: null,
1126
1157
  apiWorkflowWatch: false,
1158
+ apiWorkflowMaxCost: null,
1159
+ apiWorkflowConfirmCost: null,
1127
1160
  apiVideoPrompt: null,
1128
1161
  apiNegativePrompt: null,
1129
1162
  apiGenerateAudio: null,
@@ -1197,12 +1230,17 @@ const cliSet = {
1197
1230
  lastFrameStrength: false,
1198
1231
  apiBaseUrl: false,
1199
1232
  llmModel: false,
1233
+ apiTaskProfile: false,
1234
+ apiMaxTokens: false,
1235
+ apiThinking: false,
1200
1236
  apiTools: false,
1201
1237
  apiSystemPrompt: false,
1202
1238
  apiWorkflowKind: false,
1203
1239
  apiWorkflowInput: false,
1204
1240
  apiWorkflowTitle: false,
1205
1241
  apiWorkflowIdempotencyKey: false,
1242
+ apiWorkflowMaxCost: false,
1243
+ apiWorkflowConfirmCost: false,
1206
1244
  apiVideoPrompt: false,
1207
1245
  apiNegativePrompt: false,
1208
1246
  apiGenerateAudio: false,
@@ -1609,6 +1647,22 @@ for (let i = 0; i < args.length; i++) {
1609
1647
  i++;
1610
1648
  options.llmModel = raw;
1611
1649
  cliSet.llmModel = true;
1650
+ } else if (arg === '--task-profile') {
1651
+ const raw = requireFlagValue(args, i, arg);
1652
+ i++;
1653
+ options.apiTaskProfile = raw;
1654
+ cliSet.apiTaskProfile = true;
1655
+ } else if (arg === '--max-tokens' || arg === '--max-completion-tokens') {
1656
+ const raw = requireFlagValue(args, i, arg);
1657
+ i++;
1658
+ options.apiMaxTokens = parsePositiveIntegerValue(raw, arg);
1659
+ cliSet.apiMaxTokens = true;
1660
+ } else if (arg === '--thinking') {
1661
+ options.apiThinking = true;
1662
+ cliSet.apiThinking = true;
1663
+ } else if (arg === '--no-thinking') {
1664
+ options.apiThinking = false;
1665
+ cliSet.apiThinking = true;
1612
1666
  } else if (arg === '--api-tools') {
1613
1667
  const raw = requireFlagValue(args, i, arg);
1614
1668
  i++;
@@ -1621,6 +1675,30 @@ for (let i = 0; i < args.length; i++) {
1621
1675
  i++;
1622
1676
  options.apiSystemPrompt = raw;
1623
1677
  cliSet.apiSystemPrompt = true;
1678
+ } else if (arg === '--list-api-models' || arg === '--api-models') {
1679
+ options.apiModelAction = 'list';
1680
+ } else if (arg === '--get-api-model' || arg === '--api-model') {
1681
+ const raw = requireFlagValue(args, i, arg);
1682
+ i++;
1683
+ options.apiModelAction = 'get';
1684
+ options.apiModelId = raw;
1685
+ } else if (arg === '--list-replays' || arg === '--list-replay-records') {
1686
+ options.apiReplayAction = 'list';
1687
+ const next = args[i + 1];
1688
+ if (next && !next.startsWith('-')) {
1689
+ i++;
1690
+ options.apiReplayLimit = parsePositiveIntegerValue(next, arg);
1691
+ }
1692
+ } else if (arg === '--get-replay' || arg === '--get-replay-record') {
1693
+ const raw = requireFlagValue(args, i, arg);
1694
+ i++;
1695
+ options.apiReplayAction = 'get';
1696
+ options.apiReplayId = raw;
1697
+ } else if (arg === '--ingest-replay' || arg === '--ingest-replay-record') {
1698
+ const raw = requireFlagValue(args, i, arg);
1699
+ i++;
1700
+ options.apiReplayAction = 'ingest';
1701
+ options.apiReplayInput = raw;
1624
1702
  } else if (arg === '--api-workflow' || arg === '--creative-workflow') {
1625
1703
  const raw = requireFlagValue(args, i, arg);
1626
1704
  i++;
@@ -1642,6 +1720,17 @@ for (let i = 0; i < args.length; i++) {
1642
1720
  i++;
1643
1721
  options.apiWorkflowIdempotencyKey = raw;
1644
1722
  cliSet.apiWorkflowIdempotencyKey = true;
1723
+ } else if (arg === '--workflow-max-cost' || arg === '--max-workflow-cost') {
1724
+ const raw = requireFlagValue(args, i, arg);
1725
+ i++;
1726
+ options.apiWorkflowMaxCost = parseNonNegativeNumberValue(raw, arg);
1727
+ cliSet.apiWorkflowMaxCost = true;
1728
+ } else if (arg === '--confirm-cost') {
1729
+ options.apiWorkflowConfirmCost = true;
1730
+ cliSet.apiWorkflowConfirmCost = true;
1731
+ } else if (arg === '--no-confirm-cost') {
1732
+ options.apiWorkflowConfirmCost = false;
1733
+ cliSet.apiWorkflowConfirmCost = true;
1645
1734
  } else if (arg === '--storyboard-frames') {
1646
1735
  const raw = requireFlagValue(args, i, arg);
1647
1736
  i++;
@@ -1693,6 +1782,11 @@ for (let i = 0; i < args.length; i++) {
1693
1782
  i++;
1694
1783
  options.apiWorkflowAction = 'cancel';
1695
1784
  options.apiWorkflowId = raw;
1785
+ } else if (arg === '--resume-workflow') {
1786
+ const raw = requireFlagValue(args, i, arg);
1787
+ i++;
1788
+ options.apiWorkflowAction = 'resume';
1789
+ options.apiWorkflowId = raw;
1696
1790
  // --- Memory commands ---
1697
1791
  } else if (arg === '--memory-set') {
1698
1792
  options.memoryAction = 'set';
@@ -1868,15 +1962,25 @@ Video Options:
1868
1962
  --last-image Use last generated image as reference
1869
1963
 
1870
1964
  Hosted API Modes:
1871
- --api-chat Use /v1/chat/completions with rich creative-agent tools
1872
- --api-tools <mode> creative-agent|rich|hosted|none (default: creative-agent)
1965
+ --api-chat Use /v1/chat/completions with Sogni creative-agent tools
1966
+ --api-tools <mode> creative-agent|creative-tools|none (default: creative-agent)
1873
1967
  --no-api-tool-execution Ask for tool calls/plans but do not execute Sogni tools
1874
1968
  --llm-model <id> LLM model for --api-chat (default: ${DEFAULT_LLM_MODEL})
1969
+ --task-profile <p> LLM task profile for --api-chat: general|coding|reasoning
1970
+ --max-tokens <num> Max chat completion tokens for --api-chat and storyboard planning
1971
+ --thinking, --no-thinking Toggle chat_template_kwargs.enable_thinking
1875
1972
  --system <text> System prompt for --api-chat
1876
- --api-workflow <kind> Start /v1/creative-agent/workflows: image-to-video|hosted-tool-sequence|storyboard-video
1877
- --workflow-input <json|path|@path> JSON input for hosted-tool-sequence/custom image-to-video/storyboard-video
1878
- --workflow-title <text> Title for hosted-tool-sequence or storyboard-video workflow input
1973
+ --list-api-models List Sogni Intelligence LLM models from /v1/models
1974
+ --get-api-model <id> Fetch one Sogni Intelligence model descriptor
1975
+ --list-replays [n] List recent /v1/replay/records (default: 50)
1976
+ --get-replay <id> Fetch one replay RunRecord
1977
+ --ingest-replay <json|path|@path> POST a RunRecord to /v1/replay/records
1978
+ --api-workflow <kind> Start /v1/creative-agent/workflows: image-to-video|hosted-tool-sequence|creative-plan|storyboard-video
1979
+ --workflow-input <json|path|@path> JSON input for hosted-tool-sequence/creative-plan/custom image-to-video/storyboard-video
1980
+ --workflow-title <text> Title for hosted-tool-sequence, creative-plan, or storyboard-video workflow input
1879
1981
  --workflow-idempotency-key <key> Reuse safely when retrying a workflow start request
1982
+ --workflow-max-cost <n> Reject the workflow if estimated capacity units exceed n
1983
+ --confirm-cost, --no-confirm-cost Forward explicit workflow cost confirmation
1880
1984
  --storyboard-frames <n> Frame/beat count for --api-workflow storyboard-video
1881
1985
  --video-prompt <text> Motion prompt for --api-workflow image-to-video
1882
1986
  --negative-prompt <text> Negative prompt for --api-workflow image-to-video
@@ -1888,6 +1992,7 @@ Hosted API Modes:
1888
1992
  --workflow-events <id> Fetch workflow event history
1889
1993
  --stream-workflow <id> Stream workflow events over SSE
1890
1994
  --cancel-workflow <id> Cancel a running workflow
1995
+ --resume-workflow <id> Resume a failed, partial, waiting, or running durable workflow
1891
1996
  --api-base-url <url> Sogni API base URL (default: ${DEFAULT_API_BASE_URL})
1892
1997
 
1893
1998
  General:
@@ -1991,6 +2096,7 @@ Examples:
1991
2096
  sogni-agent --video --reference-audio-identity voice.webm 'NARRATOR: "This is my voice."'
1992
2097
  sogni-agent --api-chat "Create a 4-shot product video concept for a red sneaker"
1993
2098
  sogni-agent --api-workflow image-to-video --video-prompt "slow push-in as it comes alive" "a graphite robot sketch"
2099
+ sogni-agent --api-workflow creative-plan --workflow-input @plan.json
1994
2100
  sogni-agent --api-workflow storyboard-video --storyboard-frames 6 "Create a 12s 9:16 bakery launch video with GPT Image 2 and Seedance"
1995
2101
  sogni-agent --video -m ltx23-22b-fp8_t2v_distilled --duration 20 "A wide cinematic aerial shot opens over steep tropical cliffs at golden hour, warm sunlight grazing the rock faces while sea mist drifts above the water below. Palm trees bend gently along the ridge as waves roll against the shoreline, leaving bright bands of foam across the dark stone. The camera glides forward in one continuous pass, revealing more of the coastline as sunlight flickers across wet surfaces and distant birds wheel through the haze. The scene holds a calm, upscale travel-film mood with smooth stabilized motion and crisp environmental detail."
1996
2102
  sogni-agent --video --ref subject.jpg --ref-video motion.mp4 --workflow animate-move "transfer motion"
@@ -2050,9 +2156,36 @@ if (openclawConfig) {
2050
2156
  if (!cliSet.llmModel && openclawConfig.defaultLlmModel) {
2051
2157
  options.llmModel = openclawConfig.defaultLlmModel;
2052
2158
  }
2159
+ if (!cliSet.apiTaskProfile && openclawConfig.defaultTaskProfile) {
2160
+ options.apiTaskProfile = openclawConfig.defaultTaskProfile;
2161
+ }
2162
+ if (!cliSet.apiMaxTokens && Number.isSafeInteger(openclawConfig.defaultApiMaxTokens)) {
2163
+ if (openclawConfig.defaultApiMaxTokens < 1) {
2164
+ fatalCliError('OpenClaw config defaultApiMaxTokens must be a positive integer.', {
2165
+ code: 'INVALID_CONFIG',
2166
+ details: { field: 'defaultApiMaxTokens', value: openclawConfig.defaultApiMaxTokens }
2167
+ });
2168
+ }
2169
+ options.apiMaxTokens = openclawConfig.defaultApiMaxTokens;
2170
+ }
2171
+ if (!cliSet.apiThinking && typeof openclawConfig.defaultApiThinking === 'boolean') {
2172
+ options.apiThinking = openclawConfig.defaultApiThinking;
2173
+ }
2053
2174
  if (!cliSet.apiTools && openclawConfig.defaultApiToolMode) {
2054
2175
  options.apiTools = openclawConfig.defaultApiToolMode;
2055
2176
  }
2177
+ if (!cliSet.apiWorkflowMaxCost && isNumber(openclawConfig.defaultWorkflowMaxCost)) {
2178
+ if (openclawConfig.defaultWorkflowMaxCost < 0) {
2179
+ fatalCliError('OpenClaw config defaultWorkflowMaxCost must be a non-negative number.', {
2180
+ code: 'INVALID_CONFIG',
2181
+ details: { field: 'defaultWorkflowMaxCost', value: openclawConfig.defaultWorkflowMaxCost }
2182
+ });
2183
+ }
2184
+ options.apiWorkflowMaxCost = openclawConfig.defaultWorkflowMaxCost;
2185
+ }
2186
+ if (!cliSet.apiWorkflowConfirmCost && typeof openclawConfig.defaultWorkflowConfirmCost === 'boolean') {
2187
+ options.apiWorkflowConfirmCost = openclawConfig.defaultWorkflowConfirmCost;
2188
+ }
2056
2189
  if (!cliSet.seedStrategy && openclawConfig.seedStrategy) {
2057
2190
  options.seedStrategy = openclawConfig.seedStrategy;
2058
2191
  }
@@ -2096,9 +2229,20 @@ if (options.tokenType) {
2096
2229
  options.tokenType = token;
2097
2230
  }
2098
2231
 
2232
+ if (options.apiTaskProfile) {
2233
+ const profile = String(options.apiTaskProfile).trim().toLowerCase();
2234
+ if (!VALID_API_TASK_PROFILES.has(profile)) {
2235
+ fatalCliError('--task-profile must be "general", "coding", or "reasoning".', {
2236
+ code: 'INVALID_ARGUMENT',
2237
+ details: { flag: '--task-profile', value: options.apiTaskProfile }
2238
+ });
2239
+ }
2240
+ options.apiTaskProfile = profile;
2241
+ }
2242
+
2099
2243
  const normalizedApiToolMode = normalizeApiToolMode(options.apiTools);
2100
2244
  if (normalizedApiToolMode === null) {
2101
- fatalCliError('--api-tools must be "creative-agent", "rich", "hosted", or "none".', {
2245
+ fatalCliError('--api-tools must be "creative-agent", "creative-tools", or "none".', {
2102
2246
  code: 'INVALID_ARGUMENT',
2103
2247
  details: { flag: '--api-tools', value: options.apiTools }
2104
2248
  });
@@ -2108,7 +2252,7 @@ options.apiTools = normalizedApiToolMode;
2108
2252
  if (options.apiWorkflowKind) {
2109
2253
  const normalized = normalizeApiWorkflowKind(options.apiWorkflowKind);
2110
2254
  if (!normalized) {
2111
- fatalCliError('--api-workflow must be "image-to-video", "hosted-tool-sequence", or "storyboard-video".', {
2255
+ fatalCliError('--api-workflow must be "image-to-video", "hosted-tool-sequence", "creative-plan", or "storyboard-video".', {
2112
2256
  code: 'INVALID_ARGUMENT',
2113
2257
  details: { flag: '--api-workflow', value: options.apiWorkflowKind }
2114
2258
  });
@@ -2566,9 +2710,13 @@ if (options.music) {
2566
2710
  const apiWorkflowUtilityAction = options.apiWorkflowAction && options.apiWorkflowAction !== 'start';
2567
2711
  const apiWorkflowStartAction = options.apiWorkflowAction === 'start';
2568
2712
  const apiWorkflowStartHasExternalInput = options.apiWorkflowAction === 'start' && options.apiWorkflowInput;
2713
+ const apiModelUtilityAction = Boolean(options.apiModelAction);
2714
+ const apiReplayUtilityAction = Boolean(options.apiReplayAction);
2569
2715
  const personaUtilityAction = Boolean(options.personaAction && options.personaAction !== 'generate');
2570
2716
  const commandUsesGenerationSeed = !options.apiChat &&
2571
2717
  !apiWorkflowUtilityAction &&
2718
+ !apiModelUtilityAction &&
2719
+ !apiReplayUtilityAction &&
2572
2720
  !options.estimateVideoCost &&
2573
2721
  !options.showBalance &&
2574
2722
  !options.showVersion &&
@@ -2584,40 +2732,22 @@ if (apiWorkflowStartAction && options.apiWorkflowKind === 'image_to_video' && !o
2584
2732
  if (apiWorkflowStartAction && options.apiWorkflowKind === 'hosted_tool_sequence' && !apiWorkflowStartHasExternalInput) {
2585
2733
  fatalCliError('--api-workflow hosted-tool-sequence requires --workflow-input JSON.', { code: 'INVALID_ARGUMENT' });
2586
2734
  }
2735
+ if (apiWorkflowStartAction && options.apiWorkflowKind === 'creative_plan' && !apiWorkflowStartHasExternalInput) {
2736
+ fatalCliError('--api-workflow creative-plan requires --workflow-input JSON.', { code: 'INVALID_ARGUMENT' });
2737
+ }
2587
2738
  if (apiWorkflowStartAction && options.apiWorkflowKind === 'storyboard_video' && !options.prompt && !apiWorkflowStartHasExternalInput) {
2588
2739
  fatalCliError('--api-workflow storyboard-video requires a prompt or --workflow-input JSON.', { code: 'INVALID_ARGUMENT' });
2589
2740
  }
2590
- if (!options.prompt && !options.apiChat && !apiWorkflowUtilityAction && !apiWorkflowStartAction && !options.estimateVideoCost && !options.multiAngle && !options.showBalance && !options.showVersion && !options.extractLastFrame && !options.concatVideos && !options.listMedia && !options.memoryAction && !options.personalityAction && !personaUtilityAction) {
2741
+ if (!options.prompt && !options.apiChat && !apiWorkflowUtilityAction && !apiWorkflowStartAction && !apiModelUtilityAction && !apiReplayUtilityAction && !options.estimateVideoCost && !options.multiAngle && !options.showBalance && !options.showVersion && !options.extractLastFrame && !options.concatVideos && !options.listMedia && !options.memoryAction && !options.personalityAction && !personaUtilityAction) {
2591
2742
  fatalCliError('No prompt provided. Use --help for usage.', { code: 'INVALID_ARGUMENT' });
2592
2743
  }
2593
2744
 
2594
- if (options.apiChat && !options.prompt && options.contextImages.length === 0 && !options.refImage && !options.refImageEnd) {
2595
- fatalCliError('--api-chat requires a prompt or an image reference for vision-only planning.', { code: 'INVALID_ARGUMENT' });
2745
+ if (options.apiChat && !options.prompt && getApiModeMediaReferences().length === 0) {
2746
+ fatalCliError('--api-chat requires a prompt or media reference for planning.', { code: 'INVALID_ARGUMENT' });
2596
2747
  }
2597
2748
 
2598
- const apiMediaRefs = getApiModeMediaReferences();
2599
- const apiImageRefs = apiMediaRefs.filter(ref => ref.kind === 'image');
2600
- const apiNonImageRefs = apiMediaRefs.filter(ref => ref.kind !== 'image');
2601
- if (options.apiChat && apiNonImageRefs.length > 0) {
2602
- fatalCliError(
2603
- `--api-chat does not support ${formatApiMediaFlags(apiNonImageRefs)}. Use the direct CLI path for audio/video media workflows.`,
2604
- { code: 'UNSUPPORTED_API_MEDIA_REFERENCE' }
2605
- );
2606
- }
2607
- if (options.apiChat && options.apiToolExecution && apiImageRefs.length > 0) {
2608
- fatalCliError(
2609
- '--api-chat with server-side tool execution does not currently support image references. Use the direct CLI path for uploaded-media workflows, or pass --no-api-tool-execution for vision-only chat/planning.',
2610
- { code: 'UNSUPPORTED_API_UPLOAD_EXECUTION' }
2611
- );
2612
- }
2613
- if (options.apiWorkflowAction && apiMediaRefs.length > 0) {
2614
- fatalCliError(
2615
- `Hosted workflow API modes do not accept CLI media reference flags (${formatApiMediaFlags(apiMediaRefs)}). Use --workflow-input JSON for hosted workflow inputs, or use the direct CLI path for local media workflows.`,
2616
- { code: 'UNSUPPORTED_API_MEDIA_REFERENCE' }
2617
- );
2618
- }
2619
2749
  if (options.apiWorkflowAction === 'start' && options.apiWorkflowKind === 'image_to_video' && options.apiWorkflowTitle) {
2620
- fatalCliError('--workflow-title is currently only supported with --api-workflow hosted-tool-sequence or storyboard-video.', {
2750
+ fatalCliError('--workflow-title is currently only supported with --api-workflow hosted-tool-sequence, creative-plan, or storyboard-video.', {
2621
2751
  code: 'INVALID_ARGUMENT',
2622
2752
  details: { flag: '--workflow-title', workflow: options.apiWorkflowKind }
2623
2753
  });
@@ -3003,7 +3133,7 @@ function loadCredentials() {
3003
3133
 
3004
3134
  const err = new Error('No Sogni API key found.');
3005
3135
  err.code = 'MISSING_CREDENTIALS';
3006
- err.hint = 'Set SOGNI_API_KEY, or configure SOGNI_CREDENTIALS_PATH with SOGNI_API_KEY. You can find your API key by logging into https://dashboard.sogni.ai and clicking your username.';
3136
+ err.hint = 'Set SOGNI_API_KEY, or configure SOGNI_CREDENTIALS_PATH with SOGNI_API_KEY. You can find your API key by logging into https://dashboard.sogni.ai and opening the account menu.';
3007
3137
  err.details = {
3008
3138
  triedEnv: ['SOGNI_API_KEY'],
3009
3139
  triedFile: CREDENTIALS_PATH
@@ -3026,7 +3156,7 @@ function requireApiKeyCredentials(creds, modeLabel) {
3026
3156
  if (creds?.SOGNI_API_KEY) return creds.SOGNI_API_KEY;
3027
3157
  const err = new Error(`${modeLabel} requires SOGNI_API_KEY API-key authentication.`);
3028
3158
  err.code = 'MISSING_API_KEY';
3029
- err.hint = 'Create an API key and set SOGNI_API_KEY; username/password auth is only supported by the direct client-wrapper path.';
3159
+ err.hint = 'Create an API key and set SOGNI_API_KEY; this command only supports API-key authentication.';
3030
3160
  throw err;
3031
3161
  }
3032
3162
 
@@ -3083,6 +3213,178 @@ function formatApiMediaFlags(refs) {
3083
3213
  return [...new Set(refs.map(ref => ref.flag))].join(', ');
3084
3214
  }
3085
3215
 
3216
+ function apiMediaReferenceMaxBytes() {
3217
+ const configured = Number(getEnv('SOGNI_API_MEDIA_REFERENCE_MAX_BYTES') || '');
3218
+ return Number.isFinite(configured) && configured > 0
3219
+ ? configured
3220
+ : DEFAULT_API_MEDIA_REFERENCE_MAX_BYTES;
3221
+ }
3222
+
3223
+ function isRemoteApiMediaReference(value) {
3224
+ return /^https?:\/\//i.test(String(value || ''));
3225
+ }
3226
+
3227
+ function isInlineApiMediaReference(value) {
3228
+ return /^data:[^,]+,/i.test(String(value || ''));
3229
+ }
3230
+
3231
+ function mimeTypeForMediaReference(ref) {
3232
+ const value = String(ref.value || '');
3233
+ const clean = value.split('?')[0].toLowerCase();
3234
+ if (ref.kind === 'video') {
3235
+ if (clean.endsWith('.webm')) return 'video/webm';
3236
+ if (clean.endsWith('.m4v')) return 'video/mp4';
3237
+ }
3238
+ if (ref.kind === 'audio' && clean.endsWith('.webm')) return 'audio/webm';
3239
+ return mimeTypeForPath(value, `${ref.kind}/unknown`);
3240
+ }
3241
+
3242
+ function localApiMediaReferenceFile(ref) {
3243
+ const filePath = sanitizePath(String(ref.value || ''), `${ref.flag} media reference`);
3244
+ const stat = statSync(filePath);
3245
+ if (!stat.isFile()) {
3246
+ const err = new Error(`${ref.flag} must point to a file when using local API media references.`);
3247
+ err.code = 'INVALID_MEDIA_REFERENCE';
3248
+ throw err;
3249
+ }
3250
+ const maxBytes = apiMediaReferenceMaxBytes();
3251
+ if (stat.size > maxBytes) {
3252
+ const err = new Error(`${ref.flag} media reference is ${stat.size} bytes, above the ${maxBytes} byte API upload limit.`);
3253
+ err.code = 'MEDIA_REFERENCE_TOO_LARGE';
3254
+ throw err;
3255
+ }
3256
+ const mimeType = mimeTypeForMediaReference(ref);
3257
+ return {
3258
+ filePath,
3259
+ filename: basename(filePath),
3260
+ byteLength: stat.size,
3261
+ mimeType,
3262
+ };
3263
+ }
3264
+
3265
+ function apiMediaReferenceUploadType(ref, index) {
3266
+ if (ref.kind === 'audio') return 'referenceAudio';
3267
+ if (ref.kind === 'video') return 'referenceVideo';
3268
+ if (ref.flag === '--ref-end') return 'referenceImageEnd';
3269
+ if (ref.flag === '-c/--context') return `contextImage${Math.min(index + 1, 16)}`;
3270
+ return 'referenceImage';
3271
+ }
3272
+
3273
+ function apiMediaReferenceEndpoint(ref, action) {
3274
+ return ref.kind === 'image'
3275
+ ? `/v1/image/${action}Url`
3276
+ : `/v1/media/${action}Url`;
3277
+ }
3278
+
3279
+ function apiMediaReferenceUrlPath(ref, file, index, action, jobId) {
3280
+ const params = new URLSearchParams();
3281
+ params.set('type', apiMediaReferenceUploadType(ref, index));
3282
+ params.set('jobId', jobId);
3283
+ params.set('contentType', file.mimeType);
3284
+ if (ref.kind === 'image') {
3285
+ params.set('imageId', `media_ref_${index + 1}`);
3286
+ } else {
3287
+ params.set('id', `media_ref_${index + 1}`);
3288
+ }
3289
+ return `${apiMediaReferenceEndpoint(ref, action)}?${params.toString()}`;
3290
+ }
3291
+
3292
+ function apiStoredMediaUrl(payload, key) {
3293
+ const data = extractApiEnvelopeData(payload);
3294
+ const value = data?.[key] || payload?.[key];
3295
+ if (typeof value === 'string' && value) return value;
3296
+ const err = new Error(`Sogni API did not return ${key} for media reference upload.`);
3297
+ err.code = 'MEDIA_UPLOAD_FAILED';
3298
+ err.details = { payload };
3299
+ throw err;
3300
+ }
3301
+
3302
+ async function putApiMediaUpload(uploadUrl, file) {
3303
+ const response = await fetch(uploadUrl, {
3304
+ method: 'PUT',
3305
+ headers: { 'Content-Type': file.mimeType },
3306
+ body: readFileSync(file.filePath),
3307
+ });
3308
+ if (!response.ok) {
3309
+ const err = new Error(`Failed to upload ${file.filename} (${response.status} ${response.statusText}).`);
3310
+ err.code = 'MEDIA_UPLOAD_FAILED';
3311
+ err.details = { uploadUrl, status: response.status, statusText: response.statusText };
3312
+ throw err;
3313
+ }
3314
+ }
3315
+
3316
+ async function uploadLocalApiMediaReference(ref, index, apiKey) {
3317
+ if (!apiKey) {
3318
+ const err = new Error(`${ref.flag} local media references require SOGNI_API_KEY so the CLI can upload them before hosted execution.`);
3319
+ err.code = 'MISSING_API_KEY';
3320
+ throw err;
3321
+ }
3322
+ const file = localApiMediaReferenceFile(ref);
3323
+ const jobId = `sogni-agent-${Date.now()}-${index + 1}-${randomBytes(4).toString('hex')}`;
3324
+ const uploadPayload = await fetchApiJson(apiMediaReferenceUrlPath(ref, file, index, 'upload', jobId), { apiKey });
3325
+ const uploadUrl = apiStoredMediaUrl(uploadPayload, 'uploadUrl');
3326
+ await putApiMediaUpload(uploadUrl, file);
3327
+ const downloadPayload = await fetchApiJson(apiMediaReferenceUrlPath(ref, file, index, 'download', jobId), { apiKey });
3328
+ const url = apiStoredMediaUrl(downloadPayload, 'downloadUrl');
3329
+ return {
3330
+ url,
3331
+ filename: file.filename,
3332
+ byte_length: file.byteLength,
3333
+ mime_type: file.mimeType,
3334
+ prompt_label: file.filename,
3335
+ storage: {
3336
+ jobId,
3337
+ type: apiMediaReferenceUploadType(ref, index),
3338
+ },
3339
+ };
3340
+ }
3341
+
3342
+ async function buildApiMediaReferencePayloadItem(ref, index, apiKey) {
3343
+ const mimeType = mimeTypeForMediaReference(ref);
3344
+ const base = {
3345
+ id: `media_ref_${index + 1}`,
3346
+ source: 'cli',
3347
+ flag: ref.flag,
3348
+ kind: ref.kind,
3349
+ mime_type: mimeType,
3350
+ };
3351
+ if (isInlineApiMediaReference(ref.value)) {
3352
+ return {
3353
+ ...base,
3354
+ dataUri: ref.value,
3355
+ filename: `inline-${base.id}`,
3356
+ prompt_label: `inline-${base.id}`,
3357
+ };
3358
+ }
3359
+ if (isRemoteApiMediaReference(ref.value)) {
3360
+ return {
3361
+ ...base,
3362
+ url: ref.value,
3363
+ prompt_label: ref.value,
3364
+ };
3365
+ }
3366
+ const local = await uploadLocalApiMediaReference(ref, index, apiKey);
3367
+ return {
3368
+ ...base,
3369
+ ...local,
3370
+ filename: local.filename,
3371
+ mime_type: local.mime_type,
3372
+ };
3373
+ }
3374
+
3375
+ async function buildApiMediaReferencesPayload(refs = getApiModeMediaReferences(), { apiKey } = {}) {
3376
+ return Promise.all(refs.map((ref, index) => buildApiMediaReferencePayloadItem(ref, index, apiKey)));
3377
+ }
3378
+
3379
+ function formatApiMediaReferencesForPrompt(mediaReferences) {
3380
+ if (!mediaReferences.length) return '';
3381
+ const lines = mediaReferences.map(ref => {
3382
+ const label = ref.prompt_label || ref.url || ref.filename || ref.id;
3383
+ return `- ${ref.id} ${ref.kind} (${ref.flag}): ${label}`;
3384
+ });
3385
+ return `API media references:\n${lines.join('\n')}`;
3386
+ }
3387
+
3086
3388
  function extractApiEnvelopeData(payload) {
3087
3389
  return payload?.data && typeof payload.data === 'object' ? payload.data : payload;
3088
3390
  }
@@ -3123,51 +3425,92 @@ async function imageDataUriFromPathOrUrl(pathOrUrl) {
3123
3425
  return `data:${mimeType};base64,${buffer.toString('base64')}`;
3124
3426
  }
3125
3427
 
3126
- async function buildApiChatMessages() {
3428
+ function publicSkillApiSessionState(apiMediaReferences) {
3429
+ return {
3430
+ hasUploadedImage: apiMediaReferences.some(ref => ref.kind === 'image'),
3431
+ hasUploadedVideo: apiMediaReferences.some(ref => ref.kind === 'video'),
3432
+ hasUploadedAudio: apiMediaReferences.some(ref => ref.kind === 'audio'),
3433
+ };
3434
+ }
3435
+
3436
+ function publicSkillContractRuntimePayload(apiMediaReferences) {
3437
+ const runtime = createPublicSkillDefaultContractRuntime();
3438
+ const compiled = compilePublicSkillToolSurface({
3439
+ runtime,
3440
+ tools: PUBLIC_SKILL_DEFAULT_TOOL_DEFINITIONS,
3441
+ sessionState: publicSkillApiSessionState(apiMediaReferences),
3442
+ });
3443
+ return {
3444
+ version: 'default',
3445
+ turn_policy: {
3446
+ visible_tools: compiled.turnPolicy.visibleTools,
3447
+ forbidden_tools: compiled.turnPolicy.forbiddenTools,
3448
+ required_tools: compiled.turnPolicy.requiredTools,
3449
+ applied_policies: compiled.turnPolicy.appliedPolicies,
3450
+ rationale: compiled.turnPolicy.rationale,
3451
+ },
3452
+ contract_counts: {
3453
+ policies: runtime.policies.length,
3454
+ prompt_contracts: runtime.promptContracts.length,
3455
+ repair_recipes: runtime.repairRecipes.length,
3456
+ },
3457
+ };
3458
+ }
3459
+
3460
+ async function buildApiChatMessages(apiMediaRefs, apiMediaReferences) {
3127
3461
  const system = options.apiSystemPrompt ||
3128
3462
  'You are a concise creative production assistant. Use Sogni creative tools when they help produce concrete media.';
3129
- const imageRefs = [
3130
- ...options.contextImages,
3131
- options.refImage,
3132
- options.refImageEnd
3133
- ].filter(Boolean);
3463
+ const imageRefs = apiMediaRefs.filter(ref => ref.kind === 'image');
3464
+ const nonImageRefs = apiMediaReferences.filter(ref => ref.kind !== 'image');
3465
+ const promptText = [
3466
+ options.prompt || 'Describe the attached media.',
3467
+ formatApiMediaReferencesForPrompt(nonImageRefs)
3468
+ ].filter(Boolean).join('\n\n');
3134
3469
 
3135
3470
  const messages = [{ role: 'system', content: system }];
3136
3471
  if (imageRefs.length === 0) {
3137
- messages.push({ role: 'user', content: options.prompt });
3472
+ messages.push({ role: 'user', content: promptText });
3138
3473
  return messages;
3139
3474
  }
3140
3475
 
3141
- if (options.apiToolExecution) {
3142
- const err = new Error(
3143
- '--api-chat with server-side tool execution does not currently support image references. ' +
3144
- 'Use the direct CLI path for uploaded-media workflows, or pass --no-api-tool-execution for vision-only chat/planning.'
3145
- );
3146
- err.code = 'UNSUPPORTED_API_UPLOAD_EXECUTION';
3147
- throw err;
3148
- }
3149
-
3150
- const content = [{ type: 'text', text: options.prompt || 'Describe the attached media.' }];
3476
+ const content = [{ type: 'text', text: promptText }];
3151
3477
  for (const ref of imageRefs) {
3152
- content.push({ type: 'image_url', image_url: { url: await imageDataUriFromPathOrUrl(ref) } });
3478
+ content.push({ type: 'image_url', image_url: { url: await imageDataUriFromPathOrUrl(ref.value) } });
3153
3479
  }
3154
3480
  messages.push({ role: 'user', content });
3155
3481
  return messages;
3156
3482
  }
3157
3483
 
3484
+ function apiChatTemplateKwargs() {
3485
+ if (typeof options.apiThinking !== 'boolean') return null;
3486
+ return { enable_thinking: options.apiThinking };
3487
+ }
3488
+
3158
3489
  async function runApiChat(log) {
3159
3490
  const creds = loadCredentials();
3160
3491
  const apiKey = requireApiKeyCredentials(creds, '--api-chat');
3492
+ const apiMediaRefs = getApiModeMediaReferences();
3493
+ const apiMediaReferences = await buildApiMediaReferencesPayload(apiMediaRefs, { apiKey });
3494
+ const messages = sanitizeMessagesForLlm(await buildApiChatMessages(apiMediaRefs, apiMediaReferences));
3495
+ const chatTemplateKwargs = apiChatTemplateKwargs();
3496
+ const publicSkillRuntime = publicSkillContractRuntimePayload(apiMediaReferences);
3161
3497
  const body = {
3162
3498
  model: options.llmModel || DEFAULT_LLM_MODEL,
3163
- messages: await buildApiChatMessages(),
3499
+ messages,
3164
3500
  temperature: 0.4,
3165
- max_tokens: 1600,
3501
+ max_tokens: options.apiMaxTokens || 1600,
3166
3502
  token_type: options.tokenType || 'spark',
3167
3503
  app_source: SOGNI_APP_SOURCE,
3168
3504
  appSource: SOGNI_APP_SOURCE,
3169
3505
  sogni_tools: options.apiTools,
3170
- sogni_tool_execution: options.apiToolExecution
3506
+ sogni_tool_execution: options.apiToolExecution,
3507
+ public_skill_contract_runtime: publicSkillRuntime,
3508
+ ...(options.apiTaskProfile ? { task_profile: options.apiTaskProfile } : {}),
3509
+ ...(chatTemplateKwargs ? { chat_template_kwargs: chatTemplateKwargs } : {}),
3510
+ ...(apiMediaReferences.length > 0 ? {
3511
+ api_media_references: apiMediaReferences,
3512
+ media_references: apiMediaReferences,
3513
+ } : {})
3171
3514
  };
3172
3515
  const payload = await fetchApiJson('/v1/chat/completions', {
3173
3516
  apiKey,
@@ -3208,22 +3551,36 @@ async function runApiChat(log) {
3208
3551
  }
3209
3552
  }
3210
3553
 
3211
- function parseWorkflowInput(raw) {
3554
+ function parseJsonArgument(raw, label, code = 'INVALID_JSON_INPUT') {
3212
3555
  if (!raw) return null;
3213
3556
  const sourcePath = raw.startsWith('@') ? raw.slice(1) : raw;
3214
3557
  const expanded = expandHomePath(sourcePath);
3215
- const text = raw.startsWith('@') || existsSync(expanded)
3216
- ? readFileSync(expanded, 'utf8')
3217
- : raw;
3558
+ let text;
3559
+ if (raw.startsWith('@') || existsSync(expanded)) {
3560
+ try {
3561
+ text = readFileSync(expanded, 'utf8');
3562
+ } catch (error) {
3563
+ const err = new Error(`Unable to read ${label} file: ${error?.message || String(error)}`);
3564
+ err.code = code;
3565
+ err.details = { path: expanded };
3566
+ throw err;
3567
+ }
3568
+ } else {
3569
+ text = raw;
3570
+ }
3218
3571
  try {
3219
3572
  return JSON.parse(text);
3220
3573
  } catch (error) {
3221
- const err = new Error(`Invalid --workflow-input JSON: ${error?.message || String(error)}`);
3222
- err.code = 'INVALID_WORKFLOW_INPUT';
3574
+ const err = new Error(`Invalid ${label} JSON: ${error?.message || String(error)}`);
3575
+ err.code = code;
3223
3576
  throw err;
3224
3577
  }
3225
3578
  }
3226
3579
 
3580
+ function parseWorkflowInput(raw) {
3581
+ return parseJsonArgument(raw, '--workflow-input', 'INVALID_WORKFLOW_INPUT');
3582
+ }
3583
+
3227
3584
  function buildImageToVideoWorkflowInput() {
3228
3585
  const parsed = parseWorkflowInput(options.apiWorkflowInput);
3229
3586
  if (parsed) return parsed;
@@ -3257,6 +3614,14 @@ function buildHostedToolSequenceWorkflowInput() {
3257
3614
  return parsed;
3258
3615
  }
3259
3616
 
3617
+ function buildCreativePlanWorkflowInput() {
3618
+ const parsed = parseWorkflowInput(options.apiWorkflowInput);
3619
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && options.apiWorkflowTitle && !parsed.title) {
3620
+ parsed.title = options.apiWorkflowTitle;
3621
+ }
3622
+ return parsed;
3623
+ }
3624
+
3260
3625
  function storyboardWorkflowImageQualityFromCli() {
3261
3626
  if (!cliSet.quality || !options.quality) return undefined;
3262
3627
  if (options.quality === 'pro') return 'high';
@@ -3355,17 +3720,21 @@ function buildStoryboardStorylineMessages() {
3355
3720
  }
3356
3721
 
3357
3722
  async function generateStoryboardWorkflowStoryline(apiKey) {
3723
+ const messages = sanitizeMessagesForLlm(buildStoryboardStorylineMessages());
3724
+ const chatTemplateKwargs = apiChatTemplateKwargs();
3358
3725
  const payload = await fetchApiJson('/v1/chat/completions', {
3359
3726
  apiKey,
3360
3727
  method: 'POST',
3361
3728
  body: {
3362
3729
  model: options.llmModel || DEFAULT_LLM_MODEL,
3363
- messages: buildStoryboardStorylineMessages(),
3730
+ messages,
3364
3731
  temperature: 0.45,
3365
- max_tokens: 1800,
3732
+ max_tokens: options.apiMaxTokens || 1800,
3366
3733
  token_type: options.tokenType || 'spark',
3367
3734
  app_source: SOGNI_APP_SOURCE,
3368
3735
  appSource: SOGNI_APP_SOURCE,
3736
+ ...(options.apiTaskProfile ? { task_profile: options.apiTaskProfile } : {}),
3737
+ ...(chatTemplateKwargs ? { chat_template_kwargs: chatTemplateKwargs } : {}),
3369
3738
  sogni_tools: false,
3370
3739
  sogni_tool_execution: false
3371
3740
  }
@@ -3423,6 +3792,104 @@ function eventsFromPayload(payload) {
3423
3792
  return data?.events || payload?.events || [];
3424
3793
  }
3425
3794
 
3795
+ function modelsFromPayload(payload) {
3796
+ if (Array.isArray(payload?.data)) return payload.data;
3797
+ const data = extractApiEnvelopeData(payload);
3798
+ const models = Array.isArray(data) ? data : data?.models;
3799
+ return Array.isArray(models) ? models : [];
3800
+ }
3801
+
3802
+ async function runApiModels() {
3803
+ const creds = loadCredentials();
3804
+ const type = 'api-models';
3805
+ const action = options.apiModelAction || 'list';
3806
+ const apiKey = requireApiKeyCredentials(creds, action === 'get' ? '--get-api-model' : '--list-api-models');
3807
+ const payload = action === 'get'
3808
+ ? await fetchApiJson(`/v1/models/${encodeURIComponent(options.apiModelId)}`, { apiKey })
3809
+ : await fetchApiJson('/v1/models', { apiKey });
3810
+
3811
+ if (options.json) {
3812
+ console.log(JSON.stringify({
3813
+ success: true,
3814
+ type,
3815
+ action,
3816
+ ...(action === 'get' ? { model: payload } : { models: modelsFromPayload(payload) }),
3817
+ raw: payload
3818
+ }));
3819
+ return;
3820
+ }
3821
+
3822
+ if (action === 'get') {
3823
+ console.log(JSON.stringify(payload, null, 2));
3824
+ return;
3825
+ }
3826
+
3827
+ const models = modelsFromPayload(payload);
3828
+ for (const model of models) {
3829
+ console.log(`${model.id || model.modelId || model.name || '(unknown)'}\t${model.owned_by || model.displayName || ''}`);
3830
+ }
3831
+ }
3832
+
3833
+ function recordsFromReplayPayload(payload) {
3834
+ const data = extractApiEnvelopeData(payload);
3835
+ return Array.isArray(data?.records) ? data.records : Array.isArray(payload?.records) ? payload.records : [];
3836
+ }
3837
+
3838
+ function replayRecordFromPayload(payload) {
3839
+ const data = extractApiEnvelopeData(payload);
3840
+ return data?.record || payload?.record || payload;
3841
+ }
3842
+
3843
+ async function runApiReplay() {
3844
+ const creds = loadCredentials();
3845
+ const type = 'api-replay';
3846
+ const action = options.apiReplayAction || 'list';
3847
+ const replayModeLabel = action === 'get'
3848
+ ? '--get-replay'
3849
+ : action === 'ingest'
3850
+ ? '--ingest-replay'
3851
+ : '--list-replays';
3852
+ const apiKey = requireApiKeyCredentials(creds, replayModeLabel);
3853
+ let payload;
3854
+
3855
+ if (action === 'list') {
3856
+ payload = await fetchApiJson(`/v1/replay/records?limit=${encodeURIComponent(options.apiReplayLimit || 50)}`, { apiKey });
3857
+ const records = recordsFromReplayPayload(payload);
3858
+ if (options.json) {
3859
+ console.log(JSON.stringify({ success: true, type, action, records, raw: payload }));
3860
+ } else {
3861
+ for (const record of records) {
3862
+ console.log(`${record.runId || record.run_id || '(unknown)'}\t${record.modelId || record.model_id || '-'}\t${record.rounds ?? '-'}\t${record.userRequest || record.user_request || ''}`);
3863
+ }
3864
+ }
3865
+ return;
3866
+ }
3867
+
3868
+ if (action === 'get') {
3869
+ payload = await fetchApiJson(`/v1/replay/records/${encodeURIComponent(options.apiReplayId)}`, { apiKey });
3870
+ const record = replayRecordFromPayload(payload);
3871
+ if (options.json) {
3872
+ console.log(JSON.stringify({ success: true, type, action, runId: options.apiReplayId, record, raw: payload }));
3873
+ } else {
3874
+ console.log(JSON.stringify(record, null, 2));
3875
+ }
3876
+ return;
3877
+ }
3878
+
3879
+ const recordInput = parseJsonArgument(options.apiReplayInput, '--ingest-replay', 'INVALID_REPLAY_INPUT');
3880
+ payload = await fetchApiJson('/v1/replay/records', {
3881
+ apiKey,
3882
+ method: 'POST',
3883
+ body: recordInput
3884
+ });
3885
+ const result = extractApiEnvelopeData(payload);
3886
+ if (options.json) {
3887
+ console.log(JSON.stringify({ success: true, type, action, result, raw: payload }));
3888
+ } else {
3889
+ console.log(`Replay record ingested: ${result.runId || result.run_id || recordInput?.run_id || '(unknown)'}`);
3890
+ }
3891
+ }
3892
+
3426
3893
  function printWorkflowSummary(workflow) {
3427
3894
  console.log(`Workflow: ${workflow.workflowId || workflow.id || '(unknown)'}`);
3428
3895
  if (workflow.kind) console.log(`Kind: ${workflow.kind}`);
@@ -3536,7 +4003,13 @@ async function runApiWorkflow() {
3536
4003
  return;
3537
4004
  }
3538
4005
 
3539
- if (options.apiWorkflowAction === 'get' || options.apiWorkflowAction === 'events' || options.apiWorkflowAction === 'stream' || options.apiWorkflowAction === 'cancel') {
4006
+ if (
4007
+ options.apiWorkflowAction === 'get'
4008
+ || options.apiWorkflowAction === 'events'
4009
+ || options.apiWorkflowAction === 'stream'
4010
+ || options.apiWorkflowAction === 'cancel'
4011
+ || options.apiWorkflowAction === 'resume'
4012
+ ) {
3540
4013
  const id = options.apiWorkflowId;
3541
4014
  if (!id) {
3542
4015
  const err = new Error('Workflow id is required.');
@@ -3554,10 +4027,12 @@ async function runApiWorkflow() {
3554
4027
  ? `/v1/creative-agent/workflows/${encodeURIComponent(id)}/events`
3555
4028
  : options.apiWorkflowAction === 'cancel'
3556
4029
  ? `/v1/creative-agent/workflows/${encodeURIComponent(id)}/cancel`
3557
- : `/v1/creative-agent/workflows/${encodeURIComponent(id)}`;
4030
+ : options.apiWorkflowAction === 'resume'
4031
+ ? `/v1/creative-agent/workflows/${encodeURIComponent(id)}/resume`
4032
+ : `/v1/creative-agent/workflows/${encodeURIComponent(id)}`;
3558
4033
  payload = await fetchApiJson(path, {
3559
4034
  apiKey,
3560
- method: options.apiWorkflowAction === 'cancel' ? 'POST' : 'GET'
4035
+ method: options.apiWorkflowAction === 'cancel' || options.apiWorkflowAction === 'resume' ? 'POST' : 'GET'
3561
4036
  });
3562
4037
  if (options.apiWorkflowAction === 'events') {
3563
4038
  const events = eventsFromPayload(payload);
@@ -3571,6 +4046,8 @@ async function runApiWorkflow() {
3571
4046
  return;
3572
4047
  }
3573
4048
 
4049
+ const apiMediaReferences = await buildApiMediaReferencesPayload(undefined, { apiKey });
4050
+ const publicSkillRuntime = publicSkillContractRuntimePayload(apiMediaReferences);
3574
4051
  const requestedKind = options.apiWorkflowKind || 'image_to_video';
3575
4052
  let kind = requestedKind;
3576
4053
  let input;
@@ -3586,7 +4063,9 @@ async function runApiWorkflow() {
3586
4063
  } else {
3587
4064
  input = requestedKind === 'hosted_tool_sequence'
3588
4065
  ? buildHostedToolSequenceWorkflowInput()
3589
- : buildImageToVideoWorkflowInput();
4066
+ : requestedKind === 'creative_plan'
4067
+ ? buildCreativePlanWorkflowInput()
4068
+ : buildImageToVideoWorkflowInput();
3590
4069
  }
3591
4070
 
3592
4071
  payload = await fetchApiJson('/v1/creative-agent/workflows', {
@@ -3599,9 +4078,19 @@ async function runApiWorkflow() {
3599
4078
  kind,
3600
4079
  input,
3601
4080
  ...(options.apiWorkflowIdempotencyKey ? { idempotency_key: options.apiWorkflowIdempotencyKey } : {}),
4081
+ ...(apiMediaReferences.length > 0 ? {
4082
+ api_media_references: apiMediaReferences,
4083
+ media_references: apiMediaReferences,
4084
+ } : {}),
4085
+ ...(options.apiWorkflowMaxCost !== null ? {
4086
+ max_estimated_capacity_units: options.apiWorkflowMaxCost,
4087
+ cost_ceiling: options.apiWorkflowMaxCost,
4088
+ } : {}),
4089
+ ...(options.apiWorkflowConfirmCost !== null ? { confirm_cost: options.apiWorkflowConfirmCost } : {}),
3602
4090
  token_type: tokenType,
3603
4091
  app_source: SOGNI_APP_SOURCE,
3604
- appSource: SOGNI_APP_SOURCE
4092
+ appSource: SOGNI_APP_SOURCE,
4093
+ public_skill_contract_runtime: publicSkillRuntime
3605
4094
  }
3606
4095
  });
3607
4096
  const workflow = workflowFromPayload(payload);
@@ -4940,6 +5429,16 @@ async function main() {
4940
5429
  }
4941
5430
  }
4942
5431
 
5432
+ if (options.apiModelAction) {
5433
+ await runApiModels();
5434
+ return;
5435
+ }
5436
+
5437
+ if (options.apiReplayAction) {
5438
+ await runApiReplay();
5439
+ return;
5440
+ }
5441
+
4943
5442
  if (options.apiChat) {
4944
5443
  await runApiChat(log);
4945
5444
  return;
@@ -6019,11 +6518,11 @@ async function main() {
6019
6518
  exitCode = 1;
6020
6519
  const shouldJson = options.json || IS_OPENCLAW_INVOCATION;
6021
6520
  if (shouldJson) {
6022
- const payload = {
6521
+ const payload = addCanonicalErrorFields({
6023
6522
  success: false,
6024
6523
  error: error.message,
6025
6524
  prompt: options.prompt ?? null
6026
- };
6525
+ }, error);
6027
6526
  if (error.code) payload.errorCode = error.code;
6028
6527
  if (error.details) payload.errorDetails = error.details;
6029
6528
  if (error.hint) payload.hint = error.hint;