davinci-resolve-mcp 2.135.0 → 2.136.1

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/docs/README.md CHANGED
@@ -41,57 +41,63 @@ ignored scratch folders such as `docs/_scratch/`.
41
41
  - [Project Lifecycle](kernels/project-lifecycle-kernel.md)
42
42
  - [Extension Authoring](kernels/extension-authoring-kernel.md)
43
43
 
44
- ## Claude Code Skills
45
-
46
- Per-domain skills in `.claude/skills/` route craft ↔ live tools ↔ offline
47
- advanced tools automatically when an agent works in that domain. They are thin
48
- bridges — the authoritative depth stays in the kernels and guides above.
49
-
50
- - `resolve-mcp` (`.claude/skills/resolve-mcp/SKILL.md`) — orientation/index: the map to the domain skills below (self-trigger; not an auto-loader)
51
- - `resolve-color` (`.claude/skills/resolve-color/SKILL.md`) — grading, looks, shot match, LUT/CDL/DRX
52
- - `resolve-edit` (`.claude/skills/resolve-edit/SKILL.md`) — cutting, ranges, variants, changelist
53
- - `resolve-conform` (`.claude/skills/resolve-conform/SKILL.md`) — conform, relink, finishing QC, grade tracing
54
- - `resolve-delivery` (`.claude/skills/resolve-delivery/SKILL.md`) — render, deliverable QC, media/provenance
55
- - `resolve-fusion` (`.claude/skills/resolve-fusion/SKILL.md`) — Fusion comps (titles, motion graphics, VFX)
56
- - `resolve-audio` (`.claude/skills/resolve-audio/SKILL.md`) — audio/Fairlight tracks, buses, loudness, sync
57
- - `resolve-media-pool` (`.claude/skills/resolve-media-pool/SKILL.md`) — media pool ingest, organize, multicam
58
- - `resolve-media-analysis` (`.claude/skills/resolve-media-analysis/SKILL.md`) — source-safe media intelligence
59
-
60
- Each skill is a directory containing `SKILL.md`. Claude Code does not discover
61
- loose `.md` files in `.claude/skills/`; a skill placed at the top level of that
62
- directory silently never loads.
44
+ ## Portable Agent Skills
45
+
46
+ Per-domain skills in `.agents/skills/` route craft ↔ live tools ↔ offline
47
+ advanced tools automatically when an agent works in that domain. This is the
48
+ canonical, client-neutral source. Codex discovers it directly; Claude Code loads
49
+ `.claude/skills/` copies that are CONTENT-COMPLETE and byte-identical to the
50
+ canonical files — Claude routes on the frontmatter description at selection
51
+ time, so a pointer stub would degrade skill routing. Edit either copy, then run
52
+ `scripts/agent-rules/sync_portable_assets.py`; the parity guard
53
+ (`tests/test_portable_asset_parity.py`) fails the suite on drift.
54
+
55
+ - `resolve-mcp` — orientation/index: the map to the domain skills below (self-trigger; not an auto-loader)
56
+ - `resolve-color` — grading, looks, shot match, LUT/CDL/DRX
57
+ - `resolve-edit` — cutting, ranges, variants, changelist
58
+ - `resolve-conform` — conform, relink, finishing QC, grade tracing
59
+ - `resolve-delivery` — render, deliverable QC, media/provenance
60
+ - `resolve-fusion` — Fusion comps (titles, motion graphics, VFX)
61
+ - `resolve-audio` — audio/Fairlight tracks, buses, loudness, sync
62
+ - `resolve-media-pool` — media pool ingest, organize, multicam
63
+ - `resolve-media-analysis` — source-safe media intelligence
64
+
65
+ Each canonical skill is a directory containing `SKILL.md`. The Claude adapter
66
+ uses the same directory shape because Claude Code does not discover loose files.
63
67
 
64
68
  Two end-to-end assembly recipes sit alongside the domain skills. Where a domain
65
69
  skill routes, these two walk a whole job:
66
70
 
67
- - `resolve-rough-cut` (`.claude/skills/resolve-rough-cut/SKILL.md`) — **additive**:
71
+ - `resolve-rough-cut` — **additive**:
68
72
  select shots from a folder of many clips into an assembled timeline.
69
- - `resolve-tighten-recording` (`.claude/skills/resolve-tighten-recording/SKILL.md`)
73
+ - `resolve-tighten-recording`
70
74
  — **subtractive**: remove dead air from one long single-take recording.
71
75
 
72
76
  Three more sit outside the domain routing:
73
77
 
74
- - `house-style` (`.claude/skills/house-style/SKILL.md`) — accumulated editorial
75
- corrections, so the same note is not given twice. Claude-only; append to it
78
+ - `house-style` — accumulated editorial
79
+ corrections, so the same note is not given twice. Append to it
76
80
  when an editorial decision is corrected.
77
- - `resolve-session` (`.claude/skills/resolve-session/SKILL.md`) — `/resolve-session`
81
+ - `resolve-session` — `/resolve-session`
78
82
  connects, confirms edition and bridge, and reports project/timeline/pool state.
79
- - `release-check` (`.claude/skills/release-check/SKILL.md`) — `/release-check`
83
+ - `release-check` — `/release-check`
80
84
  walks a version bump using [docs/process/release-process.md](process/release-process.md)
81
85
  as the sole source; the skill wraps that doc, it does not duplicate it.
82
86
 
83
87
  The offline half of every one is the advanced server; see
84
88
  [Advanced Server](../resolve-advanced/README.md).
85
89
 
86
- ## Claude Code Hooks and Subagents
90
+ ## Portable Hooks and Reviewers
87
91
 
88
- Four hooks live in `.claude/hooks/` — two `PreToolUse` guards enforcing rules
89
- `AGENTS.md` states in prose, and two `PostToolUse` checks that surface
90
- engineering drift right after the edit that caused it instead of at the next
91
- test run. All four ship as scripts but are **not** wired up by default — the
92
- repository does not enable hooks on your behalf. Opt in by adding the block
93
- below to your own `.claude/settings.local.json` (gitignored, so it stays
94
- yours):
92
+ Four canonical hooks live in `.agents/hooks/` — two `PreToolUse` guards
93
+ enforcing rules `AGENTS.md` states in prose, and two `PostToolUse` checks that
94
+ surface engineering drift immediately after the edit that caused it. Thin
95
+ wrappers in `.claude/hooks/` and `.codex/hooks/` translate only the host
96
+ protocol; policy stays in the canonical scripts.
97
+
98
+ Codex wiring is committed in `.codex/hooks.json`. Because project hooks execute
99
+ commands, review and trust that file through Codex's `/hooks` UI. Claude Code
100
+ users opt in by adding this block to `.claude/settings.local.json`:
95
101
 
96
102
  ```json
97
103
  {
@@ -139,18 +145,18 @@ yours):
139
145
  }
140
146
  ```
141
147
 
142
- Hooks are read at session start, so restart Claude Code after adding them. The
143
- four are:
148
+ Hooks are read at session start, so restart the client after enabling them. The
149
+ four canonical policies are:
144
150
 
145
- - `.claude/hooks/frame_verification_guard.py` — denies grade-applying actions on
151
+ - `.agents/hooks/frame_verification_guard.py` — denies grade-applying actions on
146
152
  `timeline_item_color` until the session has actually looked at a
147
153
  Resolve-rendered frame, and asks before whole-grade artifacts
148
154
  (`safe_copy_grade`, `bulk_match_to_hero`) overwrite hand-work. `dry_run`
149
155
  passes through untouched.
150
- - `.claude/hooks/source_media_guard.py` — denies shell commands that write,
156
+ - `.agents/hooks/source_media_guard.py` — denies shell commands that write,
151
157
  move, or delete source media outside a scratch root. Reads (`ffprobe`, and
152
158
  `ffmpeg` writing into scratch) pass.
153
- - `.claude/hooks/agent_rules_drift_check.py` — after an edit to anything
159
+ - `.agents/hooks/agent_rules_drift_check.py` — after an edit to anything
154
160
  `generate.mjs` reads (`docs/SKILL.md`, `docs/kernels/README.md`,
155
161
  `resolve-advanced/README.md`, and `generate.mjs` itself, which carries the
156
162
  DOMAINS manifest inline) or to `AGENTS.md`, which it writes, runs
@@ -160,7 +166,7 @@ four are:
160
166
  blocks. Skips quietly if `node` isn't on `PATH`. **If `generate.mjs` grows a
161
167
  new input, add it to `SOURCE_PATHS` in the hook** — an unwatched input is a
162
168
  silent hook on exactly the edit it exists to catch.
163
- - `.claude/hooks/run_matching_test.py` — after an edit to `src/<module>.py`,
169
+ - `.agents/hooks/run_matching_test.py` — after an edit to `src/<module>.py`,
164
170
  runs the matching `tests/test_<module>.py` if one exists, using the project
165
171
  venv (`venv/bin/python`) so `pytest` is actually importable. Informational
166
172
  only; skips quietly if there's no matching test file or no working `pytest`.
@@ -169,8 +175,13 @@ four are:
169
175
  for `src/server.py` or `src/granular/common.py`. Silence means "no matching
170
176
  test file", not "this edit is fine".
171
177
 
172
- Three review subagents in `.claude/agents/` run in their own context so bulky
173
- output (frame images, full test transcripts) stays out of the main session:
178
+ Codex does not currently support an interactive `ask` result from `PreToolUse`.
179
+ For the two whole-grade actions, its adapter adds a warning while the MCP
180
+ server's confirmation-token gate remains authoritative. Hard denials, including
181
+ missing frame evidence and source-media writes, block in both clients.
182
+
183
+ Three canonical reviewer roles live in `.agents/roles/`, with native Codex
184
+ definitions under `.codex/agents/` and Claude adapters under `.claude/agents/`:
174
185
 
175
186
  - `cut-reviewer` — screens an assembled timeline from its frames and reports on
176
187
  pacing, shot order, continuity, and coverage gaps.
package/docs/SKILL.md CHANGED
@@ -207,7 +207,7 @@ Operating rules an agent must know:
207
207
 
208
208
  Per-domain depth for both servers lives in the kernels (`docs/kernels/`), each of
209
209
  which carries an "Advanced (offline) server" section where an offline counterpart
210
- exists. Claude Code skills in `.claude/skills/` (`resolve-color`, `resolve-edit`,
210
+ exists. Portable skills in `.agents/skills/` (`resolve-color`, `resolve-edit`,
211
211
  `resolve-conform`, `resolve-delivery`, `resolve-media-analysis`) route
212
212
  craft ↔ live ↔ offline automatically when working in that domain.
213
213
 
@@ -38,7 +38,7 @@ two:
38
38
  - Timeline Edit → `editorial` (interchange + changelist), `drt`
39
39
 
40
40
  Per-domain Claude Code skills route craft ↔ live ↔ offline automatically and live
41
- in `.claude/skills/`: `resolve-color`, `resolve-edit`, `resolve-conform`,
41
+ in `.agents/skills/`: `resolve-color`, `resolve-edit`, `resolve-conform`,
42
42
  `resolve-delivery` (plus `resolve-media-analysis`). `docs/SKILL.md` is the
43
43
  top-level brief for both servers.
44
44
 
@@ -121,7 +121,7 @@ patches audio **offline, no Resolve running**:
121
121
 
122
122
  Rule of thumb: plan/measure offline, apply mix/track changes live; use
123
123
  `fairlight` for bus work the scripting API can't reach. See the `resolve-audio`
124
- skill (`.claude/skills/resolve-audio/SKILL.md`) and the `/audio_workflow` prompt.
124
+ skill (`.agents/skills/resolve-audio/SKILL.md`) and the `/audio_workflow` prompt.
125
125
 
126
126
  ## Live Probe
127
127
 
@@ -156,7 +156,7 @@ Cross-server rules an agent must know:
156
156
  - **Deps.** The grading catalog needs `sharp`; call the advanced `capabilities`
157
157
  tool for live status and install hints.
158
158
 
159
- See the `resolve-color` skill (`.claude/skills/resolve-color/SKILL.md`) for the
159
+ See the `resolve-color` skill (`.agents/skills/resolve-color/SKILL.md`) for the
160
160
  craft ↔ live ↔ offline routing and the frame-first rule.
161
161
 
162
162
  ## Live Probe
@@ -119,7 +119,7 @@ running**, via the `fusion` tool:
119
119
  Rule of thumb: author and verify the comp offline, then apply it live with
120
120
  `fusion_comp` `safe_add_tool` → `safe_set_inputs` → `safe_connect_tools` (the
121
121
  `to_api_calls` output maps directly onto those). See the `resolve-fusion` skill
122
- (`.claude/skills/resolve-fusion/SKILL.md`) and the `/fusion_workflow` prompt.
122
+ (`.agents/skills/resolve-fusion/SKILL.md`) and the `/fusion_workflow` prompt.
123
123
 
124
124
  ## Live Probe
125
125
 
@@ -137,7 +137,7 @@ actions):
137
137
  Rule of thumb: verify and inventory the card offline *before* importing, then
138
138
  import/organize live. `media` also serves the delivery side (see the
139
139
  `resolve-delivery` skill). See the `resolve-media-pool` skill
140
- (`.claude/skills/resolve-media-pool/SKILL.md`) and the `/media_pool_workflow` prompt. Never
140
+ (`.agents/skills/resolve-media-pool/SKILL.md`) and the `/media_pool_workflow` prompt. Never
141
141
  rename or derive camera originals without explicit approval.
142
142
 
143
143
  ## Live Evidence
@@ -135,7 +135,7 @@ Rules an agent must know:
135
135
  - **Deps.** `deliverable`/`media` QC needs **ffmpeg + ffprobe on PATH** (GPL, not
136
136
  bundled) — call the advanced `capabilities` tool for status + install hints.
137
137
 
138
- See the `resolve-delivery` skill (`.claude/skills/resolve-delivery/SKILL.md`) for the
138
+ See the `resolve-delivery` skill (`.agents/skills/resolve-delivery/SKILL.md`) for the
139
139
  craft ↔ live ↔ offline routing.
140
140
 
141
141
  ## Live Evidence
@@ -160,7 +160,7 @@ Gotchas the live path shares:
160
160
  - **Deps.** `better-sqlite3` gates lineage/reverse/DB; `sharp`/ffmpeg gate frame
161
161
  compare — call the advanced `capabilities` tool.
162
162
 
163
- See the `resolve-conform` skill (`.claude/skills/resolve-conform/SKILL.md`) for the
163
+ See the `resolve-conform` skill (`.agents/skills/resolve-conform/SKILL.md`) for the
164
164
  craft ↔ live ↔ offline routing.
165
165
 
166
166
  ## Live Probe
@@ -196,7 +196,7 @@ Use these to answer "what changed between v3 and v4" or to hand a conform an
196
196
  accurate change list without opening either timeline. For conforming/relinking
197
197
  that change list, see the Timeline Conform / Interchange kernel and the
198
198
  `resolve-conform` skill; for the edit ↔ offline routing, see the `resolve-edit`
199
- skill (`.claude/skills/resolve-edit/SKILL.md`).
199
+ skill (`.agents/skills/resolve-edit/SKILL.md`).
200
200
 
201
201
  ## Development Guardrails
202
202
 
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "2.135.0"
40
+ VERSION = "2.136.1"
41
41
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
42
42
  # Resolve's scripting bridge loads into newer interpreters on recent builds
43
43
  # (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.135.0",
3
+ "version": "2.136.1",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -24,7 +24,8 @@ cannot drift.
24
24
  | Platform | File(s) | Trigger | Source |
25
25
  |---|---|---|---|
26
26
  | Codex / OpenCode / Zed / others | `AGENTS.md` (`## Domain Routing` block) | always-on | generated block |
27
- | Claude Code | `.claude/skills/<name>/SKILL.md` | semantic (description) | hand-authored (rich) |
27
+ | Codex and compatible agents | `.agents/skills/<name>/SKILL.md` | semantic (description) | hand-authored canonical skill |
28
+ | Claude Code | `.claude/skills/<name>/SKILL.md` | semantic (description) | thin adapter to canonical skill |
28
29
  | Cursor | `.cursor/rules/*.mdc` | `alwaysApply` repo rule + per-domain `description` (agent-requested) | generated |
29
30
  | VS Code / Copilot | `.github/copilot-instructions.md` + `.github/instructions/*.instructions.md` | always-on + `applyTo` | generated |
30
31
  | Windsurf | `.windsurf/rules/*.md` + `.windsurfrules` | rules dir + legacy flat | generated |
@@ -33,8 +34,9 @@ cannot drift.
33
34
  | Continue | `.continue/rules/resolve-mcp.md` | always-on | generated |
34
35
  | Claude Desktop | — (chat client, no repo rules) | — | MCP prompts only |
35
36
 
36
- `.claude/skills/*` stays hand-authored (Claude's rich semantic skills). Each
37
- one is a **directory** holding a `SKILL.md` named for its frontmatter `name`;
37
+ `.agents/skills/*` stays hand-authored as the canonical rich skill source.
38
+ `.claude/skills/*` contains only compatibility adapters. Each skill is a
39
+ **directory** holding a `SKILL.md` named for its frontmatter `name`;
38
40
  a loose `.md` at the top level of `.claude/skills/` is never discovered and
39
41
  fails silently. Every
40
42
  other file is generated. `AGENTS.md` remains the universal backstop for any
@@ -65,7 +67,7 @@ node scripts/agent-rules/generate.mjs --check # exit 1 if anything is stale (C
65
67
 
66
68
  - **New domain:** add an entry to `DOMAINS` in `generate.mjs`, add a matching
67
69
  `@mcp.prompt` in `src/server.py`, and (optionally) a rich
68
- `.claude/skills/<name>/SKILL.md`. Regenerate.
70
+ `.agents/skills/<name>/SKILL.md` and add/update its client adapter. Regenerate.
69
71
  - **New platform:** add an `emit(...)` for its convention in `generate.mjs`,
70
72
  reusing `domainBody(d)` / `repoHygiene`. Regenerate. Do not hand-edit generated
71
73
  files — the drift guard will fail.
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env python3
2
+ """Sync the portable agent assets (`.agents/`) with their Claude adapters.
3
+
4
+ The layout contract (adapted from PR #173):
5
+
6
+ - `.agents/skills/*/SKILL.md` is the canonical, host-neutral skill corpus —
7
+ the `knowledge` MCP tool serves it, and non-Claude hosts read it directly.
8
+ - `.claude/skills/*/SKILL.md` is a CONTENT-COMPLETE copy, byte-identical to
9
+ the canonical file. Claude Code routes on the frontmatter description at
10
+ selection time, so the adapter must carry the full text — a pointer stub
11
+ would cost a read per invocation and hide the trigger phrasing.
12
+ - `.agents/roles/*.md` is the body (frontmatter stripped) of the matching
13
+ `.claude/agents/*.md`. The Claude file keeps its frontmatter — name,
14
+ description, tools, and the deliberate `model:` pin — which host-neutral
15
+ role files cannot carry.
16
+ - `.agents/hooks/*.py` is canonical executable logic; `.claude/hooks` and
17
+ `.codex/hooks` are thin runpy shims (execution-only surfaces need no
18
+ content duplication).
19
+
20
+ Edit EITHER side of a skill (they are identical); edit `.claude/agents` for
21
+ roles. Then run this script to restore the invariant:
22
+
23
+ venv/bin/python scripts/agent-rules/sync_portable_assets.py # write
24
+ venv/bin/python scripts/agent-rules/sync_portable_assets.py --check # verify only
25
+
26
+ `tests/test_portable_asset_parity.py` fails the suite when the invariant is
27
+ broken, so drift cannot land.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import argparse
33
+ import re
34
+ import sys
35
+ from pathlib import Path
36
+
37
+ REPO = Path(__file__).resolve().parents[2]
38
+ CLAUDE_SKILLS = REPO / ".claude" / "skills"
39
+ AGENT_SKILLS = REPO / ".agents" / "skills"
40
+ CLAUDE_AGENTS = REPO / ".claude" / "agents"
41
+ AGENT_ROLES = REPO / ".agents" / "roles"
42
+
43
+ _FRONTMATTER_RE = re.compile(r"^---\n.*?\n---\n\n?", re.S)
44
+
45
+
46
+ def strip_frontmatter(text: str) -> str:
47
+ m = _FRONTMATTER_RE.match(text)
48
+ return text[m.end():] if m else text
49
+
50
+
51
+ def skill_pairs():
52
+ names = sorted(
53
+ {p.parent.name for p in CLAUDE_SKILLS.glob("*/SKILL.md")}
54
+ | {p.parent.name for p in AGENT_SKILLS.glob("*/SKILL.md")}
55
+ )
56
+ for name in names:
57
+ yield name, CLAUDE_SKILLS / name / "SKILL.md", AGENT_SKILLS / name / "SKILL.md"
58
+
59
+
60
+ def role_pairs():
61
+ names = sorted(
62
+ {p.stem for p in CLAUDE_AGENTS.glob("*.md")} | {p.stem for p in AGENT_ROLES.glob("*.md")}
63
+ )
64
+ for name in names:
65
+ yield name, CLAUDE_AGENTS / f"{name}.md", AGENT_ROLES / f"{name}.md"
66
+
67
+
68
+ def run(check_only: bool) -> int:
69
+ problems: list[str] = []
70
+ for name, claude_p, agent_p in skill_pairs():
71
+ if not claude_p.exists():
72
+ problems.append(f"skill '{name}': canonical exists but .claude adapter is missing — it would never load in Claude Code")
73
+ if not check_only and agent_p.exists():
74
+ claude_p.parent.mkdir(parents=True, exist_ok=True)
75
+ claude_p.write_text(agent_p.read_text(encoding="utf-8"), encoding="utf-8")
76
+ continue
77
+ src = claude_p.read_text(encoding="utf-8")
78
+ if not agent_p.exists() or agent_p.read_text(encoding="utf-8") != src:
79
+ problems.append(f"skill '{name}': .agents copy {'missing' if not agent_p.exists() else 'differs'}")
80
+ if not check_only:
81
+ agent_p.parent.mkdir(parents=True, exist_ok=True)
82
+ agent_p.write_text(src, encoding="utf-8")
83
+ for name, claude_p, role_p in role_pairs():
84
+ if not claude_p.exists():
85
+ problems.append(f"role '{name}': .agents/roles exists but .claude/agents is missing — restore the frontmatter file by hand (it carries tools/model pins this script cannot invent)")
86
+ continue
87
+ body = strip_frontmatter(claude_p.read_text(encoding="utf-8"))
88
+ if not role_p.exists() or role_p.read_text(encoding="utf-8") != body:
89
+ problems.append(f"role '{name}': .agents/roles {'missing' if not role_p.exists() else 'differs from the .claude/agents body'}")
90
+ if not check_only:
91
+ role_p.parent.mkdir(parents=True, exist_ok=True)
92
+ role_p.write_text(body, encoding="utf-8")
93
+ if problems:
94
+ for p in problems:
95
+ print(("DRIFT: " if check_only else "SYNCED: ") + p)
96
+ return 1 if check_only else 0
97
+ print("portable assets in sync")
98
+ return 0
99
+
100
+
101
+ def main() -> int:
102
+ ap = argparse.ArgumentParser(description=__doc__)
103
+ ap.add_argument("--check", action="store_true", help="verify only; non-zero exit on drift")
104
+ args = ap.parse_args()
105
+ return run(check_only=args.check)
106
+
107
+
108
+ if __name__ == "__main__":
109
+ sys.exit(main())
@@ -49,7 +49,7 @@ def parse_documented_methods(docs_path: Path) -> Dict[str, Set[str]]:
49
49
  """
50
50
  classes: Dict[str, Set[str]] = {}
51
51
  current_class: str | None = None
52
- text = docs_path.read_text()
52
+ text = docs_path.read_text(encoding="utf-8")
53
53
  # Truncate at the first deprecation marker
54
54
  for marker in ("\nDeprecated Resolve API Functions",
55
55
  "\nUnsupported Resolve API Functions"):
@@ -86,7 +86,7 @@ def collect_source_text() -> str:
86
86
  if "__pycache__" in py_path.parts:
87
87
  continue
88
88
  try:
89
- parts.append(py_path.read_text())
89
+ parts.append(py_path.read_text(encoding="utf-8"))
90
90
  except (OSError, UnicodeDecodeError):
91
91
  continue
92
92
  return "\n".join(parts)
@@ -100,7 +100,7 @@ def find_broken_api_imports() -> List[Tuple[Path, int, str]]:
100
100
  if "__pycache__" in py_path.parts:
101
101
  continue
102
102
  try:
103
- for i, line in enumerate(py_path.read_text().splitlines(), 1):
103
+ for i, line in enumerate(py_path.read_text(encoding="utf-8").splitlines(), 1):
104
104
  if pattern.search(line):
105
105
  hits.append((py_path.relative_to(REPO_ROOT), i, line.strip()))
106
106
  except (OSError, UnicodeDecodeError):
@@ -199,7 +199,7 @@ def find_undocumented_method_wrappers(
199
199
  if "__pycache__" in py_path.parts:
200
200
  continue
201
201
  try:
202
- for i, line in enumerate(py_path.read_text().splitlines(), 1):
202
+ for i, line in enumerate(py_path.read_text(encoding="utf-8").splitlines(), 1):
203
203
  for m in call_pattern.finditer(line):
204
204
  name = m.group(1)
205
205
  if name in documented or name in seen:
@@ -74,7 +74,7 @@ def step(label, fn, secs=45, pm=None):
74
74
 
75
75
 
76
76
  def dump():
77
- Path(sys.argv[1]).write_text(json.dumps(RESULTS, indent=2, default=str))
77
+ Path(sys.argv[1]).write_text(json.dumps(RESULTS, indent=2, default=str), encoding="utf-8")
78
78
 
79
79
 
80
80
  def main():
package/scripts/doctor.py CHANGED
@@ -240,7 +240,7 @@ def _normalize_separators(text: str) -> str:
240
240
  def file_contains(path: Path, needles: list[str]) -> tuple[bool, str]:
241
241
  if not path.exists():
242
242
  return False, "missing"
243
- text = _normalize_separators(path.read_text(errors="replace"))
243
+ text = _normalize_separators(path.read_text(errors="replace", encoding="utf-8"))
244
244
  missing = [needle for needle in needles if _normalize_separators(needle) not in text]
245
245
  if missing:
246
246
  return False, "missing: " + ", ".join(missing)
@@ -250,7 +250,7 @@ def file_contains(path: Path, needles: list[str]) -> tuple[bool, str]:
250
250
  def version_from_server() -> str:
251
251
  if not SERVER.exists():
252
252
  return "unknown"
253
- match = re.search(r'^VERSION\s*=\s*"([^"]+)"', SERVER.read_text(errors="replace"), re.M)
253
+ match = re.search(r'^VERSION\s*=\s*"([^"]+)"', SERVER.read_text(errors="replace", encoding="utf-8"), re.M)
254
254
  return match.group(1) if match else "unknown"
255
255
 
256
256
 
@@ -139,7 +139,7 @@ def main(argv: list[str]) -> int:
139
139
  args = _parse_args(argv)
140
140
  content = render()
141
141
  if args.check:
142
- current = DOC_PATH.read_text() if DOC_PATH.exists() else ""
142
+ current = DOC_PATH.read_text(encoding="utf-8") if DOC_PATH.exists() else ""
143
143
  if current != content:
144
144
  print(
145
145
  f"STALE: {DOC_PATH.relative_to(REPO_ROOT)} is out of date.\n"
@@ -149,7 +149,7 @@ def main(argv: list[str]) -> int:
149
149
  return 1
150
150
  print(f"OK: {DOC_PATH.relative_to(REPO_ROOT)} is up to date.")
151
151
  return 0
152
- DOC_PATH.write_text(content)
152
+ DOC_PATH.write_text(content, encoding="utf-8")
153
153
  print(f"Wrote {DOC_PATH.relative_to(REPO_ROOT)}")
154
154
  return 0
155
155
 
@@ -162,7 +162,7 @@ def completed_probes(path: Path) -> Dict[str, Dict[str, Any]]:
162
162
  results: Dict[str, Dict[str, Any]] = {}
163
163
  if not path.exists():
164
164
  return results
165
- for line in path.read_text(errors="replace").splitlines():
165
+ for line in path.read_text(errors="replace", encoding="utf-8").splitlines():
166
166
  line = line.strip()
167
167
  if not line:
168
168
  continue
@@ -183,7 +183,7 @@ def last_phase(path: Path) -> Optional[Dict[str, Any]]:
183
183
  if not path.exists():
184
184
  return None
185
185
  marker = None
186
- for line in path.read_text(errors="replace").splitlines():
186
+ for line in path.read_text(errors="replace", encoding="utf-8").splitlines():
187
187
  line = line.strip()
188
188
  if not line:
189
189
  continue
@@ -197,7 +197,7 @@ def last_phase(path: Path) -> Optional[Dict[str, Any]]:
197
197
 
198
198
 
199
199
  def append(path: Path, record: Dict[str, Any]) -> None:
200
- with path.open("a") as handle:
200
+ with path.open("a", encoding="utf-8") as handle:
201
201
  handle.write(json.dumps(record, default=str) + "\n")
202
202
 
203
203
 
@@ -177,7 +177,7 @@ def main() -> int:
177
177
  wanted = set(args.probes)
178
178
  probes = [p for p in CATALOGUE if p.name in wanted]
179
179
 
180
- with args.out.open("a") as handle:
180
+ with args.out.open("a", encoding="utf-8") as handle:
181
181
  try:
182
182
  build_fixture(ctx)
183
183
  except Exception as exc:
@@ -103,7 +103,7 @@ def build_assets() -> None:
103
103
  for g in (0.0, 1.0):
104
104
  for r in (0.0, 1.0):
105
105
  lines.append(f"{min(b + 0.15, 1.0):.6f} {g:.6f} {min(r + 0.15, 1.0):.6f}")
106
- LUT.write_text("\n".join(lines) + "\n")
106
+ LUT.write_text("\n".join(lines) + "\n", encoding="utf-8")
107
107
 
108
108
 
109
109
  def frame_hashes(path: Path) -> List[str]:
@@ -338,7 +338,7 @@ def run(mode: str, out: Path) -> int:
338
338
 
339
339
 
340
340
  def compare(gui_path: Path, headless_path: Path, out: Optional[Path]) -> int:
341
- gui, headless = json.loads(gui_path.read_text()), json.loads(headless_path.read_text())
341
+ gui, headless = json.loads(gui_path.read_text(encoding="utf-8")), json.loads(headless_path.read_text(encoding="utf-8"))
342
342
  if gui["metadata"]["headless"] is not False or headless["metadata"]["headless"] is not True:
343
343
  print("REFUSE: mode metadata does not match the arguments.", file=sys.stderr)
344
344
  return 1
@@ -161,7 +161,7 @@ class CrashWatch:
161
161
  self.offset = size # rotation/truncation resets the mark
162
162
  return None
163
163
  try:
164
- with CRASH_ARCHIVE.open("r", errors="replace") as handle:
164
+ with CRASH_ARCHIVE.open("r", errors="replace", encoding="utf-8") as handle:
165
165
  handle.seek(self.offset)
166
166
  text = handle.read()
167
167
  except OSError:
@@ -172,7 +172,7 @@ class CrashWatch:
172
172
 
173
173
  def log_tail(lines: int = 80) -> str:
174
174
  try:
175
- content = RESOLVE_LOG.read_text(errors="replace").splitlines()
175
+ content = RESOLVE_LOG.read_text(errors="replace", encoding="utf-8").splitlines()
176
176
  except OSError:
177
177
  return ""
178
178
  return "\n".join(content[-lines:])
@@ -56,7 +56,7 @@ def process_name(pid):
56
56
  except Exception:
57
57
  return ""
58
58
  try: # Linux
59
- with open("/proc/%d/comm" % pid) as handle:
59
+ with open("/proc/%d/comm" % pid, encoding="utf-8") as handle:
60
60
  return handle.read().strip()
61
61
  except (OSError, IOError):
62
62
  pass
@@ -142,7 +142,7 @@ def main():
142
142
  directory = os.path.dirname(REPORT_PATH)
143
143
  if not os.path.isdir(directory):
144
144
  os.makedirs(directory)
145
- with open(REPORT_PATH, "w") as handle:
145
+ with open(REPORT_PATH, "w", encoding="utf-8") as handle:
146
146
  json.dump(probe, handle, indent=2, sort_keys=True)
147
147
  written = REPORT_PATH
148
148
  except (OSError, IOError) as exc:
@@ -231,7 +231,7 @@ def main():
231
231
  try:
232
232
  if not os.path.isdir(REPORT_DIR):
233
233
  os.makedirs(REPORT_DIR)
234
- with open(path, "w") as handle:
234
+ with open(path, "w", encoding="utf-8") as handle:
235
235
  json.dump(report, handle, indent=2, sort_keys=True)
236
236
  written = path
237
237
  except (OSError, IOError) as exc:
@@ -391,8 +391,8 @@ def _verdict(entry: Dict[str, Any], strategy: str) -> str:
391
391
 
392
392
 
393
393
  def compare(gui_path: Path, headless_path: Path, out: Optional[Path]) -> int:
394
- gui = json.loads(gui_path.read_text())
395
- headless = json.loads(headless_path.read_text())
394
+ gui = json.loads(gui_path.read_text(encoding="utf-8"))
395
+ headless = json.loads(headless_path.read_text(encoding="utf-8"))
396
396
  assert gui["metadata"]["headless"] is False, f"{gui_path} is not a GUI run"
397
397
  assert headless["metadata"]["headless"] is True, f"{headless_path} is not a headless run"
398
398
 
@@ -385,7 +385,7 @@ def run(mode: str, out: Path) -> int:
385
385
 
386
386
 
387
387
  def compare(gui_path: Path, headless_path: Path, out: Optional[Path]) -> int:
388
- gui, headless = json.loads(gui_path.read_text()), json.loads(headless_path.read_text())
388
+ gui, headless = json.loads(gui_path.read_text(encoding="utf-8")), json.loads(headless_path.read_text(encoding="utf-8"))
389
389
  assert gui["metadata"]["headless"] is False and headless["metadata"]["headless"] is True
390
390
  lines = ["# Complex-cut round trip: what each format keeps", "",
391
391
  "Two video tracks, audio, a per-item transform, clip colours, flags, item and "
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.135.0"
90
+ VERSION = "2.136.1"
91
91
  logger = logging.getLogger("davinci-resolve-mcp")
92
92
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
93
93
  logger.info(f"Detected platform: {get_platform()}")