davinci-resolve-mcp 2.53.0 → 2.54.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/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  Release history for the DaVinci Resolve MCP Server. The latest release is summarized in the root README; older entries live here to keep the README focused.
4
4
 
5
+ ## What's New in v2.54.0
6
+
7
+ Hardens the rest of the enum-keyed settings APIs against the same silent-failure
8
+ class fixed for `AutoSyncAudio` in v2.53.0 (issue #70). Several Resolve methods
9
+ key their settings dict by `resolve.<CONST>` enum attributes and silently reject
10
+ the whole call when handed plain string keys — returning `False`/`None` with
11
+ nothing applied and no error.
12
+
13
+ - **Fixed** `CreateSubtitlesFromAudio` (both `timeline_ai.create_subtitles` and
14
+ `timeline.subtitle_generation_probe`) now resolves human-readable
15
+ `autoCaptionSettings` — `language`, `preset`, `line_break`, `chars_per_line`,
16
+ `gap` — into live `SUBTITLE_*`/`AUTO_CAPTION_*` enum keys/values. Unknown keys
17
+ and unresolvable values are dropped and reported in `ignored_settings` instead
18
+ of poisoning the call, and generation is read-back verified against the
19
+ timeline's subtitle track count (the boolean return is unreliable).
20
+ - **Fixed** the `ProjectManager` CloudProject family (`create`/`load`/
21
+ `import_project`/`restore`) resolves `{cloudSettings}` into live
22
+ `CLOUD_SETTING_*`/`CLOUD_SYNC_*` enums (`project_name`, `media_path`,
23
+ `is_collab`, `sync_mode`, `is_camera_access`), dropping unknown keys into
24
+ `ignored_settings`.
25
+ - **Changed** the string→enum resolution is now a shared `_resolve_enum_settings`
26
+ primitive driven by per-field specs, so the next enum-keyed API gets the same
27
+ treatment without re-implementing it.
28
+ - **Added** `api_truth` entries for `Timeline.CreateSubtitlesFromAudio` and the
29
+ CloudProject family, documenting the silent-rejection + unreliable-return
30
+ behavior alongside the existing `AutoSyncAudio` entry.
31
+
5
32
  ## What's New in v2.53.0
6
33
 
7
34
  Two improvements to audio sync reliability and inventory control.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # DaVinci Resolve MCP Server
2
2
 
3
- [![Version](https://img.shields.io/badge/version-2.53.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.54.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
4
4
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
5
5
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
6
6
  [![Tools](https://img.shields.io/badge/MCP%20Tools-34%20(341%20full)-blue.svg)](#server-modes)
@@ -163,3 +163,8 @@ Samuel Gursky (samgursky@gmail.com)
163
163
  - Blackmagic Design for DaVinci Resolve and its scripting API
164
164
  - The Model Context Protocol team for enabling AI assistant integration
165
165
  - Anthropic for Claude Code, used extensively in development and testing
166
+
167
+ ### Community contributions
168
+
169
+ - [@rgxdev](https://github.com/rgxdev) — configurable Media Pool inventory walk
170
+ (exclude bins + inventory limit), [#69](https://github.com/samuelgursky/davinci-resolve-mcp/pull/69) (v2.53.0)
package/docs/SKILL.md CHANGED
@@ -1005,7 +1005,12 @@ helpers:
1005
1005
  Unrecognized keys are dropped and echoed back in `ignored_settings` rather than
1006
1006
  silently failing the call.
1007
1007
  - `transcription_capabilities(clip_ids?|selected?)`
1008
- - `subtitle_generation_probe(settings?, allow_generate?)`
1008
+ - `subtitle_generation_probe(settings?, allow_generate?)` — `settings` accepts
1009
+ human-readable keys resolved to live `SUBTITLE_*`/`AUTO_CAPTION_*` enums:
1010
+ `language` (e.g. `english`, `korean`), `preset` (`default`|`teletext`|`netflix`),
1011
+ `line_break` (`single`|`double`), `chars_per_line` (1–60), `gap` (0–10).
1012
+ Unrecognized keys/values are dropped and echoed in `ignored_settings`; generation
1013
+ is read-back verified against the subtitle track count.
1009
1014
  - `fairlight_boundary_report`
1010
1015
 
1011
1016
  **`timeline_markers`** — Markers and playhead on the current timeline.
@@ -1036,7 +1041,9 @@ file to disk.
1036
1041
 
1037
1042
  **`timeline_ai`** — AI/ML analysis on the current timeline.
1038
1043
 
1039
- Key actions: `create_subtitles(settings?)`, `detect_scene_cuts`,
1044
+ Key actions: `create_subtitles(settings?)` (human-readable settings resolved to
1045
+ `SUBTITLE_*`/`AUTO_CAPTION_*` enums + read-back verified — see
1046
+ `subtitle_generation_probe`), `detect_scene_cuts`,
1040
1047
  `grab_still`, `grab_all_stills(source?)`, `analyze_dolby_vision`
1041
1048
 
1042
1049
  ---
package/install.py CHANGED
@@ -35,7 +35,7 @@ from src.utils.update_check import (
35
35
 
36
36
  # ─── Version ──────────────────────────────────────────────────────────────────
37
37
 
38
- VERSION = "2.53.0"
38
+ VERSION = "2.54.0"
39
39
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
40
40
  # Resolve's scripting bridge loads into newer interpreters on recent builds
41
41
  # (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.53.0",
3
+ "version": "2.54.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
80
80
  handlers=[logging.StreamHandler()],
81
81
  )
82
82
 
83
- VERSION = "2.53.0"
83
+ VERSION = "2.54.0"
84
84
  logger = logging.getLogger("davinci-resolve-mcp")
85
85
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
86
86
  logger.info(f"Detected platform: {get_platform()}")
package/src/server.py CHANGED
@@ -11,7 +11,7 @@ Usage:
11
11
  python src/server.py --full # Start the 341-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "2.53.0"
14
+ VERSION = "2.54.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -6040,6 +6040,182 @@ def _normalize_auto_sync_settings(settings: Dict[str, Any], resolve_obj=None):
6040
6040
  return normalized, ignored
6041
6041
 
6042
6042
 
6043
+ def _resolve_enum_settings(resolve_obj, settings, field_specs):
6044
+ """Resolve a human-readable settings dict into the live enum-keyed dict that
6045
+ enum-keyed Resolve APIs require.
6046
+
6047
+ Several Resolve methods (``MediaPool.AutoSyncAudio``,
6048
+ ``Timeline.CreateSubtitlesFromAudio``, the ``ProjectManager`` CloudProject
6049
+ family) key their settings dict by ``resolve.<CONST>`` enum attributes — and
6050
+ for many fields expect ``resolve.<CONST>`` enum *values* too. They silently
6051
+ reject the WHOLE call when handed a plain string key (returning False/None
6052
+ with nothing applied), so unrecognized keys must be dropped, not forwarded.
6053
+ See utils/api_truth.py for the catalogued symbols.
6054
+
6055
+ ``field_specs`` is an iterable of dicts, one per supported field:
6056
+ ``aliases`` human keys accepted for this field (case-insensitive)
6057
+ ``key_const`` name of the ``resolve`` enum attribute used as the dict key
6058
+ ``key_fallback`` key used when the live handle can't resolve ``key_const``
6059
+ (defaults to ``key_const``); keeps offline behavior readable
6060
+ ``values`` optional ``{lowercased_str: enum_const_name}`` value map
6061
+ ``coerce`` optional ``"int"`` | ``"bool"`` cast for plain values
6062
+ ``bounds`` optional ``(min, max)`` clamp applied after int coercion
6063
+
6064
+ Returns ``(normalized, ignored)``. ``ignored`` lists input keys that no spec
6065
+ claimed AND recognized keys whose value could not be resolved (sorted) — so a
6066
+ field that silently fails to apply is reported rather than hidden.
6067
+ """
6068
+ if not settings:
6069
+ return {}, []
6070
+ normalized = {}
6071
+ consumed = set()
6072
+ lower = {str(k).strip().lower(): k for k in settings}
6073
+
6074
+ for spec in field_specs:
6075
+ src_key = next((lower[a.lower()] for a in spec["aliases"] if a.lower() in lower), None)
6076
+ if src_key is None:
6077
+ continue
6078
+ raw = settings[src_key]
6079
+ key = _resolve_audio_constant(resolve_obj, spec["key_const"], spec.get("key_fallback", spec["key_const"]))
6080
+ values_map = spec.get("values")
6081
+ if values_map is not None and isinstance(raw, str):
6082
+ const_name = values_map.get(raw.strip().lower())
6083
+ if const_name is None:
6084
+ continue # unknown enum value — leave src_key unconsumed -> ignored
6085
+ value = _resolve_audio_constant(resolve_obj, const_name, raw)
6086
+ elif spec.get("coerce") == "int":
6087
+ try:
6088
+ value = int(raw) if not isinstance(raw, bool) else int(raw)
6089
+ except (TypeError, ValueError):
6090
+ continue
6091
+ lo, hi = spec.get("bounds", (None, None))
6092
+ if lo is not None:
6093
+ value = max(lo, value)
6094
+ if hi is not None:
6095
+ value = min(hi, value)
6096
+ elif spec.get("coerce") == "bool":
6097
+ value = bool(raw)
6098
+ else:
6099
+ value = raw
6100
+ normalized[key] = value
6101
+ consumed.add(src_key)
6102
+
6103
+ ignored = sorted(str(key) for key in settings if key not in consumed)
6104
+ return normalized, ignored
6105
+
6106
+
6107
+ # Timeline.CreateSubtitlesFromAudio({autoCaptionSettings}) — enum-keyed exactly
6108
+ # like AutoSyncAudio (docs/reference/resolve_scripting_api.txt lines 733-771).
6109
+ _AUTO_CAPTION_LANGUAGES = {
6110
+ "auto": "AUTO_CAPTION_AUTO",
6111
+ "danish": "AUTO_CAPTION_DANISH",
6112
+ "dutch": "AUTO_CAPTION_DUTCH",
6113
+ "english": "AUTO_CAPTION_ENGLISH",
6114
+ "french": "AUTO_CAPTION_FRENCH",
6115
+ "german": "AUTO_CAPTION_GERMAN",
6116
+ "italian": "AUTO_CAPTION_ITALIAN",
6117
+ "japanese": "AUTO_CAPTION_JAPANESE",
6118
+ "korean": "AUTO_CAPTION_KOREAN",
6119
+ "mandarin_simplified": "AUTO_CAPTION_MANDARIN_SIMPLIFIED",
6120
+ "mandarin_traditional": "AUTO_CAPTION_MANDARIN_TRADITIONAL",
6121
+ "norwegian": "AUTO_CAPTION_NORWEGIAN",
6122
+ "portuguese": "AUTO_CAPTION_PORTUGUESE",
6123
+ "russian": "AUTO_CAPTION_RUSSIAN",
6124
+ "spanish": "AUTO_CAPTION_SPANISH",
6125
+ "swedish": "AUTO_CAPTION_SWEDISH",
6126
+ }
6127
+ _AUTO_CAPTION_PRESETS = {
6128
+ "default": "AUTO_CAPTION_SUBTITLE_DEFAULT",
6129
+ "subtitle_default": "AUTO_CAPTION_SUBTITLE_DEFAULT",
6130
+ "teletext": "AUTO_CAPTION_TELETEXT",
6131
+ "netflix": "AUTO_CAPTION_NETFLIX",
6132
+ }
6133
+ _AUTO_CAPTION_LINE_BREAKS = {
6134
+ "single": "AUTO_CAPTION_LINE_SINGLE",
6135
+ "double": "AUTO_CAPTION_LINE_DOUBLE",
6136
+ }
6137
+ _AUTO_CAPTION_FIELD_SPECS = (
6138
+ {"aliases": ["language", "lang", "SUBTITLE_LANGUAGE"], "key_const": "SUBTITLE_LANGUAGE", "key_fallback": "language", "values": _AUTO_CAPTION_LANGUAGES},
6139
+ {"aliases": ["preset", "caption_preset", "SUBTITLE_CAPTION_PRESET"], "key_const": "SUBTITLE_CAPTION_PRESET", "key_fallback": "captionPreset", "values": _AUTO_CAPTION_PRESETS},
6140
+ {"aliases": ["line_break", "linebreak", "SUBTITLE_LINE_BREAK"], "key_const": "SUBTITLE_LINE_BREAK", "key_fallback": "lineBreak", "values": _AUTO_CAPTION_LINE_BREAKS},
6141
+ {"aliases": ["chars_per_line", "charsperline", "SUBTITLE_CHARS_PER_LINE"], "key_const": "SUBTITLE_CHARS_PER_LINE", "key_fallback": "charsPerLine", "coerce": "int", "bounds": (1, 60)},
6142
+ {"aliases": ["gap", "SUBTITLE_GAP"], "key_const": "SUBTITLE_GAP", "key_fallback": "gap", "coerce": "int", "bounds": (0, 10)},
6143
+ )
6144
+
6145
+
6146
+ def _normalize_auto_caption_settings(settings, resolve_obj=None):
6147
+ """Translate human-readable subtitle settings (e.g. ``{"language": "korean"}``)
6148
+ into live ``SUBTITLE_*``/``AUTO_CAPTION_*`` enum keys/values for
6149
+ ``CreateSubtitlesFromAudio``. Returns ``(normalized, ignored)``."""
6150
+ return _resolve_enum_settings(resolve_obj, dict(settings or {}), _AUTO_CAPTION_FIELD_SPECS)
6151
+
6152
+
6153
+ # ProjectManager CloudProject family — {cloudSettings} is keyed by
6154
+ # resolve.CLOUD_SETTING_* constants with resolve.CLOUD_SYNC_* sync-mode values
6155
+ # (docs/reference/resolve_scripting_api.txt lines 588-603). Same silent-rejection
6156
+ # failure mode as AutoSyncAudio when handed plain string keys.
6157
+ _CLOUD_SYNC_MODES = {
6158
+ "none": "CLOUD_SYNC_NONE",
6159
+ "proxy_only": "CLOUD_SYNC_PROXY_ONLY",
6160
+ "proxy": "CLOUD_SYNC_PROXY_ONLY",
6161
+ "proxy_and_orig": "CLOUD_SYNC_PROXY_AND_ORIG",
6162
+ "proxy_and_original": "CLOUD_SYNC_PROXY_AND_ORIG",
6163
+ }
6164
+ _CLOUD_SETTINGS_FIELD_SPECS = (
6165
+ {"aliases": ["project_name", "name", "CLOUD_SETTING_PROJECT_NAME"], "key_const": "CLOUD_SETTING_PROJECT_NAME", "key_fallback": "projectName"},
6166
+ {"aliases": ["project_media_path", "media_path", "CLOUD_SETTING_PROJECT_MEDIA_PATH"], "key_const": "CLOUD_SETTING_PROJECT_MEDIA_PATH", "key_fallback": "projectMediaPath"},
6167
+ {"aliases": ["is_collab", "collab", "CLOUD_SETTING_IS_COLLAB"], "key_const": "CLOUD_SETTING_IS_COLLAB", "key_fallback": "isCollab", "coerce": "bool"},
6168
+ {"aliases": ["sync_mode", "syncmode", "CLOUD_SETTING_SYNC_MODE"], "key_const": "CLOUD_SETTING_SYNC_MODE", "key_fallback": "syncMode", "values": _CLOUD_SYNC_MODES},
6169
+ {"aliases": ["is_camera_access", "camera_access", "CLOUD_SETTING_IS_CAMERA_ACCESS"], "key_const": "CLOUD_SETTING_IS_CAMERA_ACCESS", "key_fallback": "isCameraAccess", "coerce": "bool"},
6170
+ )
6171
+
6172
+
6173
+ def _normalize_cloud_settings(settings, resolve_obj=None):
6174
+ """Translate human-readable cloud-project settings (e.g.
6175
+ ``{"project_name": "X", "sync_mode": "proxy_only"}``) into live
6176
+ ``CLOUD_SETTING_*``/``CLOUD_SYNC_*`` enum keys/values. Returns
6177
+ ``(normalized, ignored)``."""
6178
+ return _resolve_enum_settings(resolve_obj, dict(settings or {}), _CLOUD_SETTINGS_FIELD_SPECS)
6179
+
6180
+
6181
+ def _safe_create_subtitles(tl, p: Dict[str, Any]):
6182
+ """CreateSubtitlesFromAudio with enum-resolved settings + readback.
6183
+
6184
+ The raw boolean is unreliable (like AutoSyncAudio), so we verify by reading
6185
+ the timeline's subtitle track count before/after and report the real delta.
6186
+ """
6187
+ settings, ignored = _normalize_auto_caption_settings(p.get("settings"), get_resolve())
6188
+ if p.get("dry_run", False):
6189
+ return _ok(would_create_subtitles=True, settings=settings, ignored_settings=ignored)
6190
+
6191
+ def _subtitle_track_count():
6192
+ try:
6193
+ return int(tl.GetTrackCount("subtitle") or 0)
6194
+ except Exception:
6195
+ return 0
6196
+
6197
+ res = verify_by_readback(
6198
+ mutate=lambda: tl.CreateSubtitlesFromAudio(settings),
6199
+ observe=_subtitle_track_count,
6200
+ snapshot=_subtitle_track_count,
6201
+ compare=lambda before, after: {
6202
+ "verified": bool((after or 0) > (before or 0)),
6203
+ "subtitle_tracks_before": before,
6204
+ "subtitle_tracks_after": after,
6205
+ },
6206
+ label="create_subtitles_from_audio",
6207
+ intent={"settings_keys": sorted(settings.keys()) if settings else []},
6208
+ )
6209
+ return {
6210
+ "success": res["success_raw"],
6211
+ "verified": res["verified"],
6212
+ "subtitle_tracks_before": res.get("subtitle_tracks_before"),
6213
+ "subtitle_tracks_after": res.get("subtitle_tracks_after"),
6214
+ "settings": settings,
6215
+ "ignored_settings": ignored,
6216
+ }
6217
+
6218
+
6043
6219
  def _transcription_capabilities(mp, p: Dict[str, Any]):
6044
6220
  root = mp.GetRootFolder()
6045
6221
  clips = []
@@ -6085,13 +6261,14 @@ def _transcription_capabilities(mp, p: Dict[str, Any]):
6085
6261
 
6086
6262
 
6087
6263
  def _subtitle_generation_probe(tl, p: Dict[str, Any]):
6088
- settings = dict(p.get("settings") or {})
6264
+ settings, ignored = _normalize_auto_caption_settings(p.get("settings"), get_resolve())
6089
6265
  if not p.get("allow_generate", False):
6090
- return _ok(would_generate=True, settings=settings, note="Pass allow_generate=True to call CreateSubtitlesFromAudio.")
6266
+ return _ok(would_generate=True, settings=settings, ignored_settings=ignored,
6267
+ note="Pass allow_generate=True to call CreateSubtitlesFromAudio.")
6091
6268
  if not _has_method(tl, "CreateSubtitlesFromAudio"):
6092
6269
  return _err("CreateSubtitlesFromAudio unavailable")
6093
6270
  with long_resolve_op("timeline.create_subtitles_from_audio"):
6094
- return {"success": bool(tl.CreateSubtitlesFromAudio(settings)), "settings": settings}
6271
+ return _safe_create_subtitles(tl, p)
6095
6272
 
6096
6273
 
6097
6274
  def _fairlight_boundary_report(proj, mp, tl, p: Dict[str, Any]):
@@ -13711,16 +13888,21 @@ def project_manager_cloud(action: str, params: Optional[Dict[str, Any]] = None)
13711
13888
  return _err("Could not connect to DaVinci Resolve. It was not running and auto-launch failed. Check that Resolve Studio is installed.")
13712
13889
  pm = r.GetProjectManager()
13713
13890
 
13891
+ # cloudSettings is enum-keyed (CLOUD_SETTING_*/CLOUD_SYNC_*); resolve string
13892
+ # keys against the live handle so human-readable settings aren't silently
13893
+ # rejected. Unrecognized keys are dropped and reported in ignored_settings.
13894
+ settings, ignored = _normalize_cloud_settings(p.get("settings"), r)
13895
+
13714
13896
  if action == "create":
13715
- proj = pm.CreateCloudProject(p["settings"])
13716
- return _ok(name=proj.GetName()) if proj else _err("Failed to create cloud project")
13897
+ proj = pm.CreateCloudProject(settings)
13898
+ return _ok(name=proj.GetName(), ignored_settings=ignored) if proj else _err("Failed to create cloud project")
13717
13899
  elif action == "load":
13718
- proj = pm.LoadCloudProject(p["settings"])
13719
- return _ok(name=proj.GetName()) if proj else _err("Failed to load cloud project")
13900
+ proj = pm.LoadCloudProject(settings)
13901
+ return _ok(name=proj.GetName(), ignored_settings=ignored) if proj else _err("Failed to load cloud project")
13720
13902
  elif action == "import_project":
13721
- return {"success": bool(pm.ImportCloudProject(p["path"], p["settings"]))}
13903
+ return {"success": bool(pm.ImportCloudProject(p["path"], settings)), "ignored_settings": ignored}
13722
13904
  elif action == "restore":
13723
- return {"success": bool(pm.RestoreCloudProject(p["folder_path"], p["settings"]))}
13905
+ return {"success": bool(pm.RestoreCloudProject(p["folder_path"], settings)), "ignored_settings": ignored}
13724
13906
  return _unknown(action, ["create","load","import_project","restore"])
13725
13907
 
13726
13908
 
@@ -18118,7 +18300,7 @@ def timeline_ai(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
18118
18300
 
18119
18301
  if action == "create_subtitles":
18120
18302
  with long_resolve_op("timeline_ai.create_subtitles"):
18121
- return {"success": bool(tl.CreateSubtitlesFromAudio(p.get("settings", {})))}
18303
+ return _safe_create_subtitles(tl, p)
18122
18304
  elif action == "detect_scene_cuts":
18123
18305
  with long_resolve_op("timeline_ai.detect_scene_cuts"):
18124
18306
  return {"success": bool(tl.DetectSceneCuts())}
@@ -29,6 +29,35 @@ API_TRUTH: List[Dict[str, Any]] = [
29
29
  "'Synced Audio' property (see verify_by_readback).",
30
30
  "tags": ["unreliable-return", "silent-failure", "audio", "enum"],
31
31
  },
32
+ {
33
+ "symbol": "Timeline.CreateSubtitlesFromAudio",
34
+ "object": "Timeline",
35
+ "signature": "(autoCaptionSettings) -> bool",
36
+ "reality": "Same failure mode as AutoSyncAudio: the autoCaptionSettings "
37
+ "dict is keyed by resolve.SUBTITLE_* enum constants with "
38
+ "resolve.AUTO_CAPTION_* enum values, so plain string keys like "
39
+ "{'language': 'korean'} are silently rejected (returns False, "
40
+ "no subtitle track created). The boolean is also unreliable.",
41
+ "recommended": "Resolve the SUBTITLE_*/AUTO_CAPTION_* constants via the "
42
+ "live resolve handle (server._normalize_auto_caption_settings) "
43
+ "and verify by reading the timeline's subtitle track count "
44
+ "before/after (server._safe_create_subtitles).",
45
+ "tags": ["unreliable-return", "silent-failure", "subtitle", "enum"],
46
+ },
47
+ {
48
+ "symbol": "ProjectManager CloudProject family (Create/Load/Import/RestoreCloudProject)",
49
+ "object": "ProjectManager",
50
+ "signature": "(..., cloudSettings) -> Project | bool",
51
+ "reality": "All four take an enum-keyed {cloudSettings} dict "
52
+ "(resolve.CLOUD_SETTING_* keys, resolve.CLOUD_SYNC_* sync-mode "
53
+ "values). Plain string keys are silently rejected, so a settings "
54
+ "dict built from human-readable keys yields no project / False.",
55
+ "recommended": "Resolve the CLOUD_SETTING_*/CLOUD_SYNC_* constants via the "
56
+ "live resolve handle (server._normalize_cloud_settings) "
57
+ "before calling, and treat the bool return from "
58
+ "Import/RestoreCloudProject as advisory.",
59
+ "tags": ["silent-failure", "project", "cloud", "enum"],
60
+ },
32
61
  {
33
62
  "symbol": "Composition.Paste",
34
63
  "object": "Fusion Composition",