Skip to content

feat: config profiles, integrations system, and DaVinci Resolve export#1

Open
nmbrthirteen wants to merge 3 commits into
mainfrom
feat/config-profiles-and-integrations
Open

feat: config profiles, integrations system, and DaVinci Resolve export#1
nmbrthirteen wants to merge 3 commits into
mainfrom
feat/config-profiles-and-integrations

Conversation

@nmbrthirteen
Copy link
Copy Markdown
Owner

@nmbrthirteen nmbrthirteen commented May 22, 2026

Summary

Three connected additions plus the paths cleanup they ride on top of:

  • Path centralization — replaces hand-rolled os.path.join(__file__, "..", "..", ".podcli", ...) across the Python backend with a single config.paths module. TypeScript mirrors the same env-var precedence (PODCLI_HOME, PODCLI_DATA) and propagates both vars to the Python subprocess. Also ignores the .podcli-home profile marker so absolute paths can't leak into commits.
  • Config profilespodcli config <export|import|use|status|migrate> packages the config home (knowledge, presets, assets, settings) into a portable zip and restores it elsewhere. Asset paths in registries/presets are rewritten to point at the imported copies. Imports back up the existing home before clobbering. MCP tool manage_config and /api/config/* HTTP endpoints expose the same actions. Idempotent migration moves legacy project/.podcli/cache/ content into data/cache/ on first run. Zip extraction rejects symlinks, absolute paths, and parent-traversal members.
  • Output integrations systemexperimentalbackend/services/integrations/ introduces a registry/manager for editor exporters. DaVinci Resolve is the first integration (opt-in via manage_integrations action=enable name=davinci_resolve): emits an FCPXML 1.10 project with one compound clip per short on the master timeline, V1 source + optional V2 alpha caption overlay, editable in free or Studio Resolve 20.x. Configure via /integrations.html. Real-clip flow tested end-to-end against 19 rendered shorts.
  • ProRes 4444 alpha caption overlayremotion/render.mjs --keep-overlay keeps a transparent caption overlay alongside the baked output instead of deleting it. clip_generator.generate_clip returns caption_overlay_path + cropped_source_path for editor handoff and auto-enables the overlay when the DaVinci integration is on, so enabling the integration is the only toggle users need.
  • CLI: resume cached AI clip suggestions[3/4] Selecting moments... now persists results to <home>/sessions/clips-<hash>.json. Subsequent runs of podcli process on the same video instant-resume instead of re-running Claude/Codex (saves several minutes + tokens per retry on long episodes). --no-resume opts out.

Also fixes a pre-existing paths variable shadow in cmd_process that the new module-level from config.paths import paths import would otherwise unmask (local paths = _thumb_gen(...) in the thumbnail loop turned the dict access into an UnboundLocalError).

Test plan

  • pytest tests/ — 298 passing (incl. new test_config_bundle.py covering round-trip, backup-on-failure, zip-slip rejection, legacy cache + presets migration)
  • npx tsc --noEmit clean
  • End-to-end: 19 real podcli clips → export_to_davinci_resolve → 17 KB FCPXML written → import path documented in backend/services/integrations/davinci_resolve/README.md
  • Manual: import the emitted .fcpxml into free DaVinci Resolve 20.x and confirm the 19 compound clips render identically to the baked outputs
  • Manual: enable davinci_resolve integration → run create_clip → verify _captions.mov ProRes 4444 alpha overlay lands next to the MP4
  • Manual: podcli config export ~/test.zippodcli config import ~/test.zip --home ~/.podcli-alt --activate → verify knowledge/presets/assets paths resolve correctly under the new home

Notes on scope

DaVinci Resolve integration is marked experimental in code (default_enabled = False) and in docs. The FCPXML emitter is intentionally constrained to free-tier Resolve structures — no Studio-only effects, no adjust-blend, no transform keyframes (Resolve mis-translates those). Future editor integrations (Premiere, FCP, CapCut) can subclass IntegrationBase and reuse the _shared FCPXML/IR helpers.

Summary by CodeRabbit

  • New Features

    • Added config profile management: export, import, and migrate configuration bundles
    • Introduced DaVinci Resolve integration for exporting clips to FCPXML format
    • Added caption overlay export option to preserve captions for external editors
    • Integration management system with enable/disable controls and UI
    • Environment variable support for custom home and data directories
  • Documentation

    • Added comprehensive CONTRIBUTING.md with setup, layout, and integration guidance
    • Updated README with project structure, configuration, and knowledge base details
    • Added DaVinci Resolve integration documentation with verification procedures
  • Bug Fixes

    • Improved transcript caching with unified cache system across Python and TypeScript
  • Refactor

    • Centralized path resolution using .podcli-home marker and environment variables
    • Implemented config migration utilities for legacy layouts
    • Restructured integrations as a pluggable system

Replace ad-hoc `os.path.join(__file__, "..", "..", ".podcli", ...)` patterns
across the Python backend with a single `config.paths` module that resolves
home/data/cache locations from env vars (PODCLI_HOME, PODCLI_DATA) and a
`.podcli-home` marker file. The TypeScript side mirrors the same precedence
in `src/config/paths.ts`, and `python-executor.ts` propagates both env vars
to the Python subprocess so the two layers always agree.

Also: ignore the `.podcli-home` profile marker to prevent absolute paths
from leaking into commits.
Three independent but related additions, plus the ProRes 4444 alpha caption
overlay that the editor exporter needs.

Config profiles
- `podcli config <export|import|use|status|migrate>` packages a config home
  (knowledge, presets, assets, settings) into a portable zip and restores
  it elsewhere. Asset paths in registries/presets are rewritten to point at
  the imported copies. Imports back up the existing home before clobbering.
- Idempotent legacy-cache migration runs once on startup to move stale
  `project/.podcli/cache/` content into `data/cache/`.
- Equivalent MCP tool `manage_config` and HTTP endpoints under `/api/config`.
- Bundle extraction rejects symlinks, absolute paths, and parent-traversal
  members to prevent zip-slip and out-of-tree writes.

Output integrations
- New `backend/services/integrations/` package with a registry/manager and a
  shared FCPXML/IR helper module for editor exporters.
- DaVinci Resolve integration (experimental, opt-in via `manage_integrations
  action=enable name=davinci_resolve`) emits an FCPXML 1.10 project: one
  compound clip per short on the master timeline, V1 source plus optional V2
  alpha caption overlay so cuts and captions remain editable in free or
  Studio Resolve 20.x.
- MCP tools: `manage_integrations`, `export_to_davinci_resolve`. UI page at
  `/integrations.html` toggles per-integration state.

ProRes alpha caption overlay
- `remotion/render.mjs --keep-overlay` writes a ProRes 4444 alpha overlay
  alongside the baked output instead of deleting it.
- `clip_generator.generate_clip` exposes `keep_caption_overlay` and returns
  `caption_overlay_path` + `cropped_source_path` for editor handoff. It also
  auto-enables the overlay when the davinci_resolve integration is on, so
  enabling the editor integration is the only knob users have to flip.

Also fixes a pre-existing bug exposed by the new `from config.paths import
paths` import in cli.py: a local `paths = _thumb_gen(...)` in the thumbnail
loop shadowed the module-level dict, so `cmd_process` crashed with
UnboundLocalError on any clip that triggered earlier `paths[...]` lookups.
Renamed the local to `thumb_paths`.
After Claude/Codex picks moments in `[3/4] Selecting moments...`, persist the
result to `<home>/sessions/clips-<cache_hash>.json` keyed by the same video
hash used for the transcript cache. A subsequent `podcli process` on the
same video reloads the saved suggestions instantly instead of re-running
the AI step (which can take several minutes on long episodes and bills
tokens each time).

`--no-resume` forces regeneration. Session files are only loaded when
`top_n` matches; mismatches fall through to a fresh AI call.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

📝 Walkthrough

Walkthrough

This PR introduces centralized path configuration with environment variables and marker files, adds a plugin framework for editor integrations (DaVinci Resolve), implements portable config profile export/import with legacy migration, extends Remotion rendering to export caption overlays, and refactors MCP tools with improved Zod schemas and error handling.

Changes

Path Resolution and Configuration Infrastructure

Layer / File(s) Summary
Path resolution modules (Python and TypeScript)
backend/config/paths.py, src/config/paths.ts, src/config/paths.test.ts
Introduces centralized path resolution checking PODCLI_HOME env var, then .podcli-home marker file, then project-root defaults; exports paths dict with cache, knowledge, assets, integrations, and other standard directories.
Path usage in backend services
backend/presets.py, backend/services/asset_store.py, backend/services/claude_suggest.py, backend/services/clip_generator.py, backend/services/content_generator.py, backend/services/corrections.py, backend/services/encoder.py, backend/services/thumbnail_*.py, backend/utils/prompt_files.py
Twelve backend modules updated to import and use config.paths.paths dictionary instead of constructing relative paths via __file__ or hardcoded .podcli/ prefixes.
Web server path integration
src/services/python-executor.ts, src/ui/web-server.ts
Web server and Python executor configured to pass PODCLI_HOME and PODCLI_DATA environment variables to subprocess, and corrections file path migrated to use centralized config.

Integration Framework and DaVinci Resolve

Layer / File(s) Summary
Integration base classes and registry
backend/services/integrations/base.py
Defines ToolSpec dataclass, abstract IntegrationBase class with tools() and describe() methods, and in-memory IntegrationRegistry for registration/lookup of integrations and their tools.
DaVinci Resolve integration with FCPXML export
backend/services/integrations/_shared/fcpxml.py, backend/services/integrations/_shared/timeline_ir.py, backend/services/integrations/_shared/media_probe.py, backend/services/integrations/davinci_resolve/integration.py, backend/services/integrations/davinci_resolve/emitter.py, backend/services/integrations/davinci_resolve/cli.py, backend/services/integrations/davinci_resolve/README.md
Shared utilities for media probing and FCPXML 1.10 generation; DaVinci Resolve integration implementing export_to_davinci_resolve tool with per-short compound clip layering, optional caption/logo lane support, and CLI spike for standalone FCPXML generation.
Integration enablement state management
backend/services/integrations/manager.py
IntegrationsManager class persists per-integration enabled/disabled state to integrations.json, with fallback to IntegrationRegistry defaults when state file is absent.
Integration package initialization and MCP tool registration
backend/services/integrations/__init__.py, backend/services/integrations/_shared/__init__.py, backend/services/integrations/davinci_resolve/__init__.py
Package-level exports and auto-registration of davinci_resolve integration with IntegrationRegistry at import time.

Caption Overlay Export for DaVinci Resolve

Layer / File(s) Summary
Remotion rendering with caption overlay support
remotion/render.mjs, remotion/test-render.mjs
--keep-overlay flag controls caption overlay preservation; when enabled, captions are saved next to output as <output>_captions.mov and emitted via PODCLI_OVERLAY_PATH=...; bundle cache now uses configurable PODCLI_CACHE_DIR instead of hardcoded .podcli/cache/.
Clip generator with caption overlay output
backend/services/clip_generator.py
Extends generate_clip() with keep_caption_overlay parameter; automatically enables overlay when davinci_resolve integration is active; captures overlay path from Remotion and conditionally augments ClipResult with caption_overlay_path and cropped_source_path.
MCP tools and models for caption overlay
src/handlers/create-clip.handler.ts, src/handlers/batch-clips.handler.ts, src/models/index.ts
Adds keep_caption_overlay boolean parameter to create_clip and batch_create_clips tools and batch specs; extends ClipResult model with optional overlay output paths.
Overlay functionality tests
tests/test_clip_generator.py
New test validates _kept_caption_overlay_path() naming contract; existing Remotion failure tests updated to assert tuple return (False, None) instead of boolean.

Config Bundle Export, Import, and Legacy Migration

Layer / File(s) Summary
Config bundle export/import and migration
backend/config_bundle.py
Implements portable config profile bundles via ZIP archives with manifest.json versioning and path_map for asset remapping; safe extraction prevents zip-slip and symlink attacks; legacy cache and preset directory migration with marker-based detection and dry-run support; backup/restore on import failure.
CLI config profiles command
backend/cli.py (config-related ranges)
Adds podcli config with subcommands for status, migrate, export, import, use actions; interactive questionary UI via _interactive_config() for user-guided profile management.
Backend handlers for config management
backend/main.py
Implements handle_manage_config task handler and _maybe_auto_migrate_backend to dispatch config actions and auto-run migration before task execution.
Config bundle tests
tests/test_config_bundle.py
Comprehensive test suite covering export/import round-trip, failure safety with backup restoration, legacy migration skipping/execution, status reporting without mutation, zip-slip security rejection, and unified transcript cache round-trip.

Transcript Cache Layout Migration and Knowledge Consolidation

Layer / File(s) Summary
Canonical transcript cache layout
backend/services/transcript_packer.py
Migrates from per-file MD5-based cache to hash-based canonical layout under cache/transcripts/; implements legacy fallback loading, save with hash computation, and migration routine to move existing JSON files into new layout.
Knowledge base directory consolidation
backend/services/claude_suggest.py, backend/services/content_generator.py
Knowledge base path resolution updated to use centralized paths["knowledge"] instead of relative filesystem computation.
Suggestion session persistence and resume
backend/cli.py (suggestions-related ranges)
Adds helpers for caching and resuming AI/heuristic clip suggestions based on per-video cache hash, enabling --no-resume flag to disable reuse.

Web API Routes for Config and Integrations

Layer / File(s) Summary
Config and integration management routes
src/handlers/integrations.routes.ts
Express endpoints for config status, migrate, export, and import (with multer ZIP upload, size limits, and type restrictions); integrations list and per-integration enable/disable endpoints via POST to /api/integrations/:name.
Web server config route registration
src/ui/web-server.ts
Wires config integration routes into Express via registerConfigIntegrationRoutes() call in startup.

Web UI Pages for Config and Integrations

Layer / File(s) Summary
Config profiles web UI page
src/ui/public/config.html
Browser-rendered status display with migrate/export/import UI controls and client-side API integration for config management workflows.
Output integrations web UI page
src/ui/public/integrations.html
Integration card grid with per-integration toggle switches for enable/disable POST handlers.
Web UI navigation links
src/ui/public/index.html, src/ui/public/integration.html
Adds Config and Integrations links to top-level navigation and cross-links between pages.

MCP Tool Handler Integration and Server Wiring

Layer / File(s) Summary
Integration and config MCP tools
src/handlers/integrations.handler.ts
MCP tools for manage_integrations, export_to_davinci_resolve, and manage_config with Zod parameter validation and standardized error/success responses.
Backend task handlers for integrations and config
backend/main.py
Implements handle_manage_integrations, handle_run_integration_tool, and handle_manage_config task handlers with proper error handling and traceback reporting.
Server MCP tool schema and handler refactoring
src/server.ts
Comprehensive refactoring of MCP tool Zod schemas for clarity, response body restructuring with consistent error/success shapes, improved workflow guidance formatting, and integration tool registration.
Web server integration and config management wiring
src/ui/web-server.ts
Adds legacy migration checks at startup, wires config routes, improves validation and error responses for clip/transcript handling, and routes integration/config requests to Python executor.
Python executor environment variable setup
src/services/python-executor.ts
Sets PODCLI_HOME and PODCLI_DATA environment variables when spawning Python backend process.

CLI Refactoring for Cache and Knowledge Management

Layer / File(s) Summary
CLI migration and cache/knowledge path updates
backend/cli.py
Startup auto-runs legacy migration; integrates centralized paths for cache and knowledge in banner, interactive flows, and cmd_cache clear/status commands.

Documentation Updates

Layer / File(s) Summary
Gitignore marker exclusion
.gitignore
Adds .podcli-home marker file to .gitignore.
Contribution guide and conventions
CONTRIBUTING.md
Comprehensive guide covering development setup, project layout, path/cache conventions, integration addition workflow, and security constraints.
README documentation refresh
README.md
Documents two-halves architecture, new project structure (.podcli/ vs data/), config profiles feature, configuration variables, and migration/upgrade paths.
Logger test refactoring
src/utils/logger.test.ts
Reformats test assertion across multiple lines without changing logic.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Paths now centralized with markers and keys,
Integrations bloom like clover trees,
Config bundles travel from home to home,
Captions with overlays help creators roam!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/config-profiles-and-integrations

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
backend/services/clip_generator.py (1)

900-913: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Persist the overlay artifacts outside work_dir.

caption_overlay_path and cropped_source_path point into work_dir, but the finally block deletes that directory whenever output_dir is set. In the normal CLI flow the caller gets paths to files that no longer exist, so the Resolve exporter can't consume them.

💡 Proposed fix
-        out = {
+        persisted_overlay_path = None
+        persisted_cropped_path = None
+        if keep_caption_overlay and caption_overlay_path and os.path.exists(caption_overlay_path):
+            base, _ = os.path.splitext(final_path)
+            persisted_overlay_path = f"{base}_captions.mov"
+            persisted_cropped_path = f"{base}_cropped.mp4"
+            shutil.copy2(caption_overlay_path, persisted_overlay_path)
+            shutil.copy2(cropped_path, persisted_cropped_path)
+
+        out = {
             "output_path": final_path,
             "duration": round(duration, 2),
             "file_size_mb": file_size_mb,
             "title": title,
             "start_second": start_second,
             "end_second": end_second,
             "caption_style": caption_style,
             "crop_strategy": crop_strategy,
         }
-        if keep_caption_overlay and caption_overlay_path and os.path.exists(caption_overlay_path):
-            out["caption_overlay_path"] = caption_overlay_path
-            out["cropped_source_path"] = cropped_path
+        if persisted_overlay_path and persisted_cropped_path:
+            out["caption_overlay_path"] = persisted_overlay_path
+            out["cropped_source_path"] = persisted_cropped_path
         return out

Also applies to: 915-918

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/clip_generator.py` around lines 900 - 913, The caption
overlay and cropped source artifacts (caption_overlay_path and cropped_path) are
currently inside work_dir which is removed in the finally block, so persist them
to a safe location before work_dir is deleted: if output_dir (or final_path's
parent) is set, copy/move caption_overlay_path and cropped_path to a durable
location (e.g., the same directory as final_path or a subfolder under
output_dir), update the out dict keys ("caption_overlay_path" and
"cropped_source_path") to point to the new persisted paths, and only include the
persisted paths in the returned out; ensure this logic is applied where out is
constructed (the block that builds out and the conditional that adds
caption_overlay_path/cropped_source_path) so callers receive valid,
non-temporary file paths.
src/server.ts (1)

771-824: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the resolved batch inputs in the direct fallback.

You compute resolvedClips, resolvedVideoPath, and resolvedTranscriptWords for export_selected / clip_numbers, but the non-Web-UI fallback still calls handleBatchClips(params). If the UI server is down, this path can export the wrong clips or fail with missing inputs even though resolution already succeeded.

Suggested fix
-          finalResult = await handleBatchClips(params);
+          finalResult = await handleBatchClips({
+            ...params,
+            video_path: resolvedVideoPath,
+            clips: resolvedClips,
+            transcript_words: resolvedTranscriptWords,
+          });

Also applies to: 895-900

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server.ts` around lines 771 - 824, The resolved batch inputs
(resolvedClips, resolvedVideoPath, resolvedTranscriptWords) computed for
export_selected/clip_numbers are not used in the non‑UI fallback; instead
handleBatchClips(params) is called which can use stale/missing params. Update
the fallback(s) that call handleBatchClips (and the similar block around the
895-900 region) to call handleBatchClips with the resolved values or to replace
params' fields before calling it — i.e., pass an object where clips =
resolvedClips, video_path = resolvedVideoPath, and transcript_words =
resolvedTranscriptWords (falling back to original params only if those resolved
variables are still undefined) so the exported batch uses the resolved inputs.
src/ui/web-server.ts (1)

1737-1747: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

/api/mcp/export is stripping clip fields the new export flow depends on.

This mapper only preserves a small subset of clip properties and only translates segments -> keep_segments. That drops already-normalized keep_segments from src/server.ts and also drops keep_caption_overlay, so MCP-driven multi-cut exports collapse to single ranges and overlay exports silently lose their sidecar assets.

Suggested fix
   const styledClips = clips.map((c: any) => ({
+    ...c,
     start_second: c.start_second,
     end_second: c.end_second,
     title: (c.title || "clip").slice(0, 40),
     caption_style: c.caption_style || captionStyle,
     crop_strategy: c.crop_strategy || cropStrategy,
     allow_ass_fallback: c.allow_ass_fallback === true || allowAssFallback,
-    // Preserve multi-cut segments from suggestions
-    ...(Array.isArray(c.segments) &&
-      c.segments.length > 0 && { keep_segments: c.segments }),
+    ...(Array.isArray(c.keep_segments) &&
+      c.keep_segments.length > 0 && { keep_segments: c.keep_segments }),
+    ...(Array.isArray(c.segments) &&
+      c.segments.length > 0 && { keep_segments: c.segments }),
+    keep_caption_overlay: c.keep_caption_overlay === true,
   }));
@@
       {
         video_path: videoPath,
         clips: styledClips,
         transcript_words: transcriptWords,
         output_dir: paths.output,
         logo_path: logoPath,
         outro_path: outroPath,
+        keep_caption_overlay: req.body.keep_caption_overlay === true,
         face_map: uiState.transcript?.face_map,
       },

Also applies to: 1771-1778

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/web-server.ts` around lines 1737 - 1747, The mapper in styledClips is
dropping clip properties the export flow needs (it converts segments ->
keep_segments but overwrites or omits already-normalized keep_segments and omits
keep_caption_overlay and other passthrough fields), so update the clips.map
transformation (styledClips and the similar block around the other occurrence)
to: 1) preserve an existing c.keep_segments if present and only derive
keep_segments from c.segments when c.keep_segments is absent, 2) include
c.keep_caption_overlay (or default) in the output, and 3) pass-through any other
important sidecar/overlay fields rather than stripping them—use the existing c.*
values (e.g. keep_segments, keep_caption_overlay, allow_ass_fallback) when
available instead of always replacing them.
🧹 Nitpick comments (6)
backend/services/integrations/davinci_resolve/__init__.py (1)

8-8: 💤 Low value

Consider alphabetically sorting __all__.

Ruff suggests sorting __all__ entries alphabetically. While the current order matches import order, alphabetical sorting improves consistency and maintainability.

♻️ Proposed fix
-__all__ = ["integration", "emitter"]
+__all__ = ["emitter", "integration"]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/integrations/davinci_resolve/__init__.py` at line 8, The
__all__ export list is unsorted; update the __all__ variable in the module to
list its names in alphabetical order (e.g., put "emitter" before "integration")
so the __all__ = [...] tuple/ list is alphabetically ordered for consistency and
to satisfy linters like Ruff.
backend/services/integrations/_shared/media_probe.py (1)

13-50: ⚡ Quick win

Consider combining the two ffprobe calls.

Running ffprobe twice (once for video, once for audio) is less efficient than running once with both stream selections. While this works correctly, combining them could reduce overhead, especially when probing many files.

♻️ Example combined approach
cmd = [
    "ffprobe", "-v", "error",
    "-select_streams", "v:0,a:0",
    "-show_entries", "stream=width,height,r_frame_rate,nb_frames,duration,channels:format=duration",
    "-of", "json",
    p,
]
data = json.loads(subprocess.check_output(cmd, text=True, timeout=30))
streams = data.get("streams", [])
v_stream = next((s for s in streams if s.get("width")), {})
a_stream = next((s for s in streams if s.get("channels")), {})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/integrations/_shared/media_probe.py` around lines 13 - 50,
The two separate ffprobe invocations in media_probe.py should be merged into a
single ffprobe call: call ffprobe once with "-select_streams", "v:0,a:0" and
"-show_entries",
"stream=width,height,r_frame_rate,nb_frames,duration,channels:format=duration",
parse the single JSON result (replace v_data/a_data usage with a single data
variable), then locate v_stream as the stream with width/height/r_frame_rate and
a_stream as the stream with channels (e.g., using next over data.get("streams",
[])); keep the existing logic that computes width, height, fps, fmt_duration and
duration_frames but read those values from v_stream/a_stream in the consolidated
data and include a timeout on subprocess.check_output as appropriate.
backend/services/integrations/davinci_resolve/cli.py (2)

70-72: 💤 Low value

Default output directory may not exist.

The default output path assumes data/export/davinci_resolve/ exists. If it doesn't, emitter.emit() will fail when writing the file. Consider ensuring the directory exists or documenting that users should create it.

🛡️ Proposed fix
     out = args.out or (
         REPO_ROOT / "data" / "export" / "davinci_resolve" / f"{args.title.replace(' ', '_')}.fcpxml"
     )
+    out.parent.mkdir(parents=True, exist_ok=True)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/integrations/davinci_resolve/cli.py` around lines 70 - 72,
The default output path assigned to out (out = args.out or (REPO_ROOT / "data" /
"export" / "davinci_resolve" / f"{args.title.replace(' ', '_')}.fcpxml"))
assumes the parent directory exists and will cause emitter.emit() to fail;
before calling emitter.emit() ensure the output file's parent directory exists
by creating out.parent with parents=True and exist_ok=True (or document that
users must create data/export/davinci_resolve), referencing the variables out,
REPO_ROOT, args.title, and the emitter.emit call so the directory creation
happens immediately prior to writing the file.

16-18: ⚡ Quick win

Fragile path calculation using fixed parent depth.

The REPO_ROOT calculation assumes this file is exactly 4 directories deep. If the file is moved or the structure changes, this will break silently.

♻️ More robust alternative
-REPO_ROOT = Path(__file__).resolve().parents[4]
+# Find repo root by looking for a marker file
+def _find_repo_root():
+    p = Path(__file__).resolve()
+    for parent in [p] + list(p.parents):
+        if (parent / "backend").is_dir() and (parent / "package.json").is_file():
+            return parent
+    raise RuntimeError("Could not locate repository root")
+
+REPO_ROOT = _find_repo_root()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/integrations/davinci_resolve/cli.py` around lines 16 - 18,
The REPO_ROOT calculation is fragile because parents[4] assumes fixed depth;
change it to walk up the directory tree from Path(__file__).resolve() until you
find a repository marker (e.g., a ".git" directory or the "backend" folder) and
set REPO_ROOT to that parent instead of using parents[4]; then use that computed
REPO_ROOT when checking/inserting into sys.path (symbols to update: REPO_ROOT,
Path(__file__).resolve(), and the sys.path.insert call) so the import path logic
continues to work if files are moved.
backend/services/integrations/davinci_resolve/emitter.py (2)

30-31: 💤 Low value

Document the rationale for ID starting values.

The asset IDs start at 2 and media IDs at 1000 without explanation. A brief comment would help future maintainers understand whether these specific values are significant or arbitrary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/integrations/davinci_resolve/emitter.py` around lines 30 -
31, Add a short explanatory comment above the next_asset and next_media
initializations in emitter.py that documents why asset IDs start at 2 and media
IDs at 1000 (e.g., reserved ID 1 for root/placeholder, gap for legacy IDs, or
arbitrary chosen to avoid collision), or mark them as intentionally arbitrary if
no special meaning; reference the variables next_asset and next_media and update
their nearby comment or module docstring to capture this rationale for future
maintainers.

23-33: ⚡ Quick win

Consider validating that project.shorts is non-empty.

The function proceeds directly to building the library even if project.shorts is empty, which would produce valid FCPXML with no clips. Adding an early check would provide clearer feedback.

🛡️ Proposed validation
 def emit(project: Project, out_path: Path) -> Path:
+    if not project.shorts:
+        raise ValueError("Project must contain at least one short")
     fmt_id = "r1"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/integrations/davinci_resolve/emitter.py` around lines 23 -
33, The emit function should validate that project.shorts is non-empty before
building the FCPXML; add an early check at the top of emit (before building
fmt_id/resources/compounds) that tests if not project.shorts and then raise a
clear exception (e.g., ValueError with a descriptive message) or return an
explicit error so callers know there are no clips to emit; update any callers or
tests that expect this behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/cli.py`:
- Around line 405-410: The session key currently only uses
compute_cache_hash(video_path) so passing a custom transcript (args.transcript)
can reuse stale suggestions; update the resume logic in the code that sets
cache_hash (uses services.transcript_packer.compute_cache_hash) to either (a)
disable resume when args.transcript is present by leaving cache_hash empty/None,
or (b) include the transcript and relevant config in the session key (e.g.,
compute a combined hash of video_path plus the transcript file contents/mtime
and any config flags) so functions that rely on cache_hash will never reuse
suggestions from a different transcript; apply the same change to the other
resume spots noted (the blocks around where cache_hash is computed at the other
occurrences).

In `@backend/config_bundle.py`:
- Around line 496-497: Activating a new home writes the marker but never
refreshes the module-level in-memory paths cache, so functions like
get_active_home() and later home-relative operations still return the old home;
after writing the marker in set_active_home() and any code path that calls
_marker_path().write_text(...) (including import_config(..., activate=True)),
invalidate or reload the cached paths (e.g., update the module-level paths
variable or call the existing path-loading routine) so
config.paths/get_active_home() reflect the new marker immediately in-process;
alternatively remove the import-time caching and make get_active_home() read the
marker dynamically. Ensure you update all activation sites referenced (the
functions that call _marker_path().write_text) to perform this refresh.

In `@backend/main.py`:
- Around line 476-483: The auto-migration call in _maybe_auto_migrate_backend
currently runs inline and will propagate exceptions to request handling; wrap
the import and call to auto_migrate_legacy_if_pending(quiet=True) in a
try/except that catches Exception, logs the exception (contextual message
referencing auto-migrate) and continues without re-raising so migration failures
are best-effort and non-blocking; apply the same try/except pattern to the other
identical call referenced at the other location (the occurrence around line 498)
and keep the existing early-return logic for "manage_config" tasks unchanged.
- Around line 453-454: The current except block returns the full traceback in
the API response; instead, log the full traceback on the server and emit a
sanitized error string to the client. In the except block for emit_result use
logging.exception(...) or a server logger to record traceback.format_exc() (or
call logger.exception(e)) and then call emit_result(task_id, "error",
error=f"{type(e).__name__}: {e}") or a generic message like "Internal server
error" so no stack/paths are sent to clients; update the block around
emit_result and the exception handler to perform server-side logging and a
sanitized emit_result call.

In `@backend/services/encoder.py`:
- Line 173: The docstring comment still hardcodes "data/cache/encoder.json" but
the cache path is now resolved dynamically via paths["cache"]; update the
docstring in backend/services/encoder.py (the comment at the Cached at ... line)
to reference the dynamic cache location (e.g. paths["cache"] joined with the
encoder filename) and note it is keyed by the ffmpeg binary fingerprint so the
documentation matches the actual behavior in this module.

In `@backend/services/integrations/_shared/media_probe.py`:
- Around line 13-21: The subprocess.check_output calls that run ffprobe
(building v_cmd and invoking subprocess.check_output to produce v_data) need a
timeout to avoid hanging; update the call to include a timeout parameter (e.g.,
timeout=30) and handle subprocess.TimeoutExpired if desired, and apply the same
change to the audio probe call that builds a_cmd / produces a_data so both
ffprobe invocations use a timeout.

In `@backend/services/integrations/base.py`:
- Around line 46-47: The register method on IntegrationBase currently overwrites
any existing entry in cls._instances; change register (def register(cls,
integration: IntegrationBase)) to first check if integration.name already exists
in cls._instances and if so raise a clear exception (e.g., ValueError or
RuntimeError) preventing silent overwrite; otherwise proceed to assign
cls._instances[integration.name] = integration so duplicate registrations fail
fast and surface the conflict.

In `@backend/services/integrations/davinci_resolve/integration.py`:
- Around line 64-66: The code currently allows an empty params["shorts"] to
proceed and emit a project with no clips; before calling probe_media and
building shorts (the block that starts with shorts: list[Short] = [] and the
later similar block around the probe_media loop), validate that params["shorts"]
is present and not empty (or that the resulting shorts list after filtering is
non-empty) and raise a clear validation error (e.g., ValueError or a
ValidationError) instead of continuing; apply this check both where
params["shorts"] is consumed and in the second similar loop to fail fast and
return an error response rather than success metadata.

In `@backend/utils/prompt_files.py`:
- Line 25: The module docstring and any function comments referring to
“.podcli/tmp/” are out of date—update them to reflect that temporary files are
now written to os.path.join(paths["home"], "tmp") (the variable base in this
file), so change wording to indicate the temp directory is under the configured
paths["home"]/tmp location (and adjust any troubleshooting/manual cleanup
instructions accordingly).

In `@src/config/paths.ts`:
- Around line 11-22: The resolveHome() function currently calls
readFileSync(homeMarker) without error handling which can crash the app; wrap
the readFileSync(homeMarker, "utf-8") call in a try-catch inside the
existsSync(homeMarker) block (keeping the isAbsolute(marker) check) and on any
exception treat it as a missing/invalid marker by falling back to return
resolve(projectRoot, ".podcli"); reference resolveHome(), homeMarker,
readFileSync, existsSync, and projectRoot when making the change.

In `@src/handlers/integrations.handler.ts`:
- Around line 127-133: The handler currently calls handleManageIntegrations({
action, name }) even when action is "enable" or "disable" and name is empty; add
an upfront guard in the async handler (the anonymous async ({ action, name }) =>
{ ... } block) that checks if action === "enable" || action === "disable" and if
name is falsy, then return a clear MCP error via mcpText (e.g., a descriptive
message like "Missing required 'name' for enable/disable action") instead of
calling handleManageIntegrations; this will fail fast and avoid sending an empty
name to Python.

In `@src/handlers/integrations.routes.ts`:
- Around line 44-57: The export route that calls
executor.execute("manage_config", ...) and then res.download(file, ...) must
delete the temporary bundle file after the response completes—use the
res.download callback or res.on('finish'/'close') to unlink the bundlePath/file
so the ZIPs don't accumulate; likewise, for the import route that reads
req.file.path, ensure you remove the uploaded file in a finally block after
processing (regardless of success/failure) to guarantee cleanup; reference the
export handler around executor.execute and res.download and the import handler
using req.file.path when adding the unlink logic.

In `@src/server.ts`:
- Around line 509-515: The export path in src/server.ts is not forwarding the
keep_caption_overlay flag to the /api/mcp/export endpoint, so Web-UI exports
ignore overlay-enabled renders; update the code that constructs the export
request payload for the Web-UI export path so it includes the
keep_caption_overlay property (preserve its boolean value/default), propagate it
through any intermediate structures or typed interfaces used when calling
/api/mcp/export, and ensure the server handler reading /api/mcp/export consumes
that field; specifically add keep_caption_overlay to the JSON/body sent to
/api/mcp/export wherever export requests are assembled.

In `@src/ui/public/integrations.html`:
- Around line 169-170: The current integration toggle is a non-focusable div
("toggle" element with role="switch" and data-name="${escapeHtml(int.name)}")
and must be replaced or augmented with a keyboard-accessible control: change the
element to a semantic, focusable control (e.g., a <button> or an <input
type="checkbox">) that maintains role="switch" or native semantics, ensure
aria-checked reflects int.enabled, include keyboard event handling (Space/Enter)
to toggle the state, keep the data-name attribute for identification, and ensure
visual classes ("on") are applied when toggled; update any click-only listeners
attached to the old ".toggle" to handle keyboard activation as well.

In `@src/ui/web-server.ts`:
- Around line 1643-1645: The field transcriptWordCount is returning the words
array itself due to misplaced parentheses around (uiState.transcript?.words ??
[].length); change it to return a numeric length instead by using
uiState.transcript?.words.length (or (uiState.transcript?.words ?? []).length)
so transcriptWordCount is always a number; update the expression that sets
transcriptWordCount in web-server.ts to reference
uiState.transcript?.words.length and fall back to 0 when missing.
- Around line 909-919: The current isUploadedFile check uses startsWith on
path.resolve(join(paths.working, "uploads")), which mistakenly matches sibling
directories like "uploads-backup"; change the guard in the web-server logic that
computes resolvedPath and isUploadedFile so you compare against the uploads
directory with a directory boundary (e.g., compute uploadsDir =
path.resolve(join(paths.working, "uploads")) and ensure resolvedPath ===
uploadsDir or resolvedPath.startsWith(uploadsDir + path.sep)) or use
path.relative to verify the file is inside uploads (relative does not begin with
'..'); update the isUploadedFile boolean accordingly where resolvedPath,
isSessionVideo, and paths.working are used.

---

Outside diff comments:
In `@backend/services/clip_generator.py`:
- Around line 900-913: The caption overlay and cropped source artifacts
(caption_overlay_path and cropped_path) are currently inside work_dir which is
removed in the finally block, so persist them to a safe location before work_dir
is deleted: if output_dir (or final_path's parent) is set, copy/move
caption_overlay_path and cropped_path to a durable location (e.g., the same
directory as final_path or a subfolder under output_dir), update the out dict
keys ("caption_overlay_path" and "cropped_source_path") to point to the new
persisted paths, and only include the persisted paths in the returned out;
ensure this logic is applied where out is constructed (the block that builds out
and the conditional that adds caption_overlay_path/cropped_source_path) so
callers receive valid, non-temporary file paths.

In `@src/server.ts`:
- Around line 771-824: The resolved batch inputs (resolvedClips,
resolvedVideoPath, resolvedTranscriptWords) computed for
export_selected/clip_numbers are not used in the non‑UI fallback; instead
handleBatchClips(params) is called which can use stale/missing params. Update
the fallback(s) that call handleBatchClips (and the similar block around the
895-900 region) to call handleBatchClips with the resolved values or to replace
params' fields before calling it — i.e., pass an object where clips =
resolvedClips, video_path = resolvedVideoPath, and transcript_words =
resolvedTranscriptWords (falling back to original params only if those resolved
variables are still undefined) so the exported batch uses the resolved inputs.

In `@src/ui/web-server.ts`:
- Around line 1737-1747: The mapper in styledClips is dropping clip properties
the export flow needs (it converts segments -> keep_segments but overwrites or
omits already-normalized keep_segments and omits keep_caption_overlay and other
passthrough fields), so update the clips.map transformation (styledClips and the
similar block around the other occurrence) to: 1) preserve an existing
c.keep_segments if present and only derive keep_segments from c.segments when
c.keep_segments is absent, 2) include c.keep_caption_overlay (or default) in the
output, and 3) pass-through any other important sidecar/overlay fields rather
than stripping them—use the existing c.* values (e.g. keep_segments,
keep_caption_overlay, allow_ass_fallback) when available instead of always
replacing them.

---

Nitpick comments:
In `@backend/services/integrations/_shared/media_probe.py`:
- Around line 13-50: The two separate ffprobe invocations in media_probe.py
should be merged into a single ffprobe call: call ffprobe once with
"-select_streams", "v:0,a:0" and "-show_entries",
"stream=width,height,r_frame_rate,nb_frames,duration,channels:format=duration",
parse the single JSON result (replace v_data/a_data usage with a single data
variable), then locate v_stream as the stream with width/height/r_frame_rate and
a_stream as the stream with channels (e.g., using next over data.get("streams",
[])); keep the existing logic that computes width, height, fps, fmt_duration and
duration_frames but read those values from v_stream/a_stream in the consolidated
data and include a timeout on subprocess.check_output as appropriate.

In `@backend/services/integrations/davinci_resolve/__init__.py`:
- Line 8: The __all__ export list is unsorted; update the __all__ variable in
the module to list its names in alphabetical order (e.g., put "emitter" before
"integration") so the __all__ = [...] tuple/ list is alphabetically ordered for
consistency and to satisfy linters like Ruff.

In `@backend/services/integrations/davinci_resolve/cli.py`:
- Around line 70-72: The default output path assigned to out (out = args.out or
(REPO_ROOT / "data" / "export" / "davinci_resolve" / f"{args.title.replace(' ',
'_')}.fcpxml")) assumes the parent directory exists and will cause
emitter.emit() to fail; before calling emitter.emit() ensure the output file's
parent directory exists by creating out.parent with parents=True and
exist_ok=True (or document that users must create data/export/davinci_resolve),
referencing the variables out, REPO_ROOT, args.title, and the emitter.emit call
so the directory creation happens immediately prior to writing the file.
- Around line 16-18: The REPO_ROOT calculation is fragile because parents[4]
assumes fixed depth; change it to walk up the directory tree from
Path(__file__).resolve() until you find a repository marker (e.g., a ".git"
directory or the "backend" folder) and set REPO_ROOT to that parent instead of
using parents[4]; then use that computed REPO_ROOT when checking/inserting into
sys.path (symbols to update: REPO_ROOT, Path(__file__).resolve(), and the
sys.path.insert call) so the import path logic continues to work if files are
moved.

In `@backend/services/integrations/davinci_resolve/emitter.py`:
- Around line 30-31: Add a short explanatory comment above the next_asset and
next_media initializations in emitter.py that documents why asset IDs start at 2
and media IDs at 1000 (e.g., reserved ID 1 for root/placeholder, gap for legacy
IDs, or arbitrary chosen to avoid collision), or mark them as intentionally
arbitrary if no special meaning; reference the variables next_asset and
next_media and update their nearby comment or module docstring to capture this
rationale for future maintainers.
- Around line 23-33: The emit function should validate that project.shorts is
non-empty before building the FCPXML; add an early check at the top of emit
(before building fmt_id/resources/compounds) that tests if not project.shorts
and then raise a clear exception (e.g., ValueError with a descriptive message)
or return an explicit error so callers know there are no clips to emit; update
any callers or tests that expect this behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f56e140a-d39d-4b4f-a887-30b5a0c3746f

📥 Commits

Reviewing files that changed from the base of the PR and between 61e1be1 and d4a5162.

📒 Files selected for processing (50)
  • .gitignore
  • CONTRIBUTING.md
  • README.md
  • backend/cli.py
  • backend/config/paths.py
  • backend/config_bundle.py
  • backend/main.py
  • backend/presets.py
  • backend/services/asset_store.py
  • backend/services/claude_suggest.py
  • backend/services/clip_generator.py
  • backend/services/content_generator.py
  • backend/services/corrections.py
  • backend/services/encoder.py
  • backend/services/integrations/__init__.py
  • backend/services/integrations/_shared/__init__.py
  • backend/services/integrations/_shared/fcpxml.py
  • backend/services/integrations/_shared/media_probe.py
  • backend/services/integrations/_shared/timeline_ir.py
  • backend/services/integrations/base.py
  • backend/services/integrations/davinci_resolve/README.md
  • backend/services/integrations/davinci_resolve/__init__.py
  • backend/services/integrations/davinci_resolve/cli.py
  • backend/services/integrations/davinci_resolve/emitter.py
  • backend/services/integrations/davinci_resolve/integration.py
  • backend/services/integrations/manager.py
  • backend/services/thumbnail_ai.py
  • backend/services/thumbnail_generator.py
  • backend/services/thumbnail_html.py
  • backend/services/transcript_packer.py
  • backend/utils/prompt_files.py
  • remotion/render.mjs
  • remotion/test-render.mjs
  • src/config/paths.test.ts
  • src/config/paths.ts
  • src/handlers/batch-clips.handler.ts
  • src/handlers/create-clip.handler.ts
  • src/handlers/integrations.handler.ts
  • src/handlers/integrations.routes.ts
  • src/models/index.ts
  • src/server.ts
  • src/services/python-executor.ts
  • src/ui/public/config.html
  • src/ui/public/index.html
  • src/ui/public/integration.html
  • src/ui/public/integrations.html
  • src/ui/web-server.ts
  • src/utils/logger.test.ts
  • tests/test_clip_generator.py
  • tests/test_config_bundle.py

Comment thread backend/cli.py
Comment on lines +405 to +410
# Cache hash for resume — keyed by video size+mtime, shared with transcript cache.
cache_hash = ""
try:
from services.transcript_packer import compute_cache_hash as _compute_cache_hash

cache_hash = _compute_cache_hash(video_path)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't resume suggestions when the transcript input changed.

The session key is derived only from compute_cache_hash(video_path), so podcli process episode.mp4 -t revised.json will happily reuse suggestions generated from an older transcript for the same video. That bypasses transcript edits and can render the wrong moments. At minimum, disable resume when args.transcript is supplied; ideally include transcript/config inputs in the session key.

Also applies to: 425-452, 542-547

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/cli.py` around lines 405 - 410, The session key currently only uses
compute_cache_hash(video_path) so passing a custom transcript (args.transcript)
can reuse stale suggestions; update the resume logic in the code that sets
cache_hash (uses services.transcript_packer.compute_cache_hash) to either (a)
disable resume when args.transcript is present by leaving cache_hash empty/None,
or (b) include the transcript and relevant config in the session key (e.g.,
compute a combined hash of video_path plus the transcript file contents/mtime
and any config flags) so functions that rely on cache_hash will never reuse
suggestions from a different transcript; apply the same change to the other
resume spots noted (the blocks around where cache_hash is computed at the other
occurrences).

Comment thread backend/config_bundle.py
Comment on lines +496 to +497
if activate:
_marker_path().write_text(str(target) + "\n", encoding="utf-8")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Activating a new home never refreshes the in-memory path map.

set_active_home() and import_config(..., activate=True) update .podcli-home, but get_active_home() still reads the import-time paths["home"]. In the same Python process, config status and any later home-relative operation stay pointed at the old home until restart. This shows up immediately in the new interactive config flow. Refresh config.paths after activation, or stop caching the active home at module import.

Also applies to: 512-520, 523-538

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/config_bundle.py` around lines 496 - 497, Activating a new home
writes the marker but never refreshes the module-level in-memory paths cache, so
functions like get_active_home() and later home-relative operations still return
the old home; after writing the marker in set_active_home() and any code path
that calls _marker_path().write_text(...) (including import_config(...,
activate=True)), invalidate or reload the cached paths (e.g., update the
module-level paths variable or call the existing path-loading routine) so
config.paths/get_active_home() reflect the new marker immediately in-process;
alternatively remove the import-time caching and make get_active_home() read the
marker dynamically. Ensure you update all activation sites referenced (the
functions that call _marker_path().write_text) to perform this refresh.

Comment thread backend/main.py
Comment on lines +453 to +454
except Exception as e:
emit_result(task_id, "error", error=f"{type(e).__name__}: {e}\n{traceback.format_exc()}")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not return full traceback content in API error responses.

This leaks internal paths and stack details to clients. Log traceback server-side and return a sanitized error message.

Proposed fix
     try:
         result = tool.handler(tool_params)
         emit_result(task_id, "success", data=result)
     except Exception as e:
-        emit_result(task_id, "error", error=f"{type(e).__name__}: {e}\n{traceback.format_exc()}")
+        print(traceback.format_exc(), file=sys.stderr, flush=True)
+        emit_result(task_id, "error", error=f"{type(e).__name__}: {e}")
🧰 Tools
🪛 Ruff (0.15.13)

[warning] 453-453: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/main.py` around lines 453 - 454, The current except block returns the
full traceback in the API response; instead, log the full traceback on the
server and emit a sanitized error string to the client. In the except block for
emit_result use logging.exception(...) or a server logger to record
traceback.format_exc() (or call logger.exception(e)) and then call
emit_result(task_id, "error", error=f"{type(e).__name__}: {e}") or a generic
message like "Internal server error" so no stack/paths are sent to clients;
update the block around emit_result and the exception handler to perform
server-side logging and a sanitized emit_result call.

Comment thread backend/main.py
Comment on lines +476 to +483
def _maybe_auto_migrate_backend(task_type: str, params: dict) -> None:
if task_type == "manage_config":
action = params.get("action", "status")
if action == "status" or (action == "migrate" and params.get("dry_run")):
return
from config_bundle import auto_migrate_legacy_if_pending

auto_migrate_legacy_if_pending(quiet=True)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Isolate auto-migration failures from normal task execution.

A migration exception here can fail unrelated requests before handler dispatch. This should be best-effort and non-blocking for regular tasks.

Proposed fix
 def _maybe_auto_migrate_backend(task_type: str, params: dict) -> None:
     if task_type == "manage_config":
         action = params.get("action", "status")
         if action == "status" or (action == "migrate" and params.get("dry_run")):
             return
-    from config_bundle import auto_migrate_legacy_if_pending
-
-    auto_migrate_legacy_if_pending(quiet=True)
+    try:
+        from config_bundle import auto_migrate_legacy_if_pending
+        auto_migrate_legacy_if_pending(quiet=True)
+    except Exception as e:
+        print(f"Warning: auto-migrate skipped: {e}", file=sys.stderr, flush=True)

Also applies to: 498-498

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/main.py` around lines 476 - 483, The auto-migration call in
_maybe_auto_migrate_backend currently runs inline and will propagate exceptions
to request handling; wrap the import and call to
auto_migrate_legacy_if_pending(quiet=True) in a try/except that catches
Exception, logs the exception (contextual message referencing auto-migrate) and
continues without re-raising so migration failures are best-effort and
non-blocking; apply the same try/except pattern to the other identical call
referenced at the other location (the occurrence around line 498) and keep the
existing early-return logic for "manage_config" tasks unchanged.

"""Get full encoder detection info (for UI/logging).

Cached at .podcli/cache/encoder.json keyed by ffmpeg binary fingerprint.
Cached at data/cache/encoder.json keyed by ffmpeg binary fingerprint.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update cache-location docstring to match the new dynamic path source.

The text still says data/cache/encoder.json, but the file now resolves via paths["cache"] (env-configurable). Keeping this accurate avoids debugging confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/encoder.py` at line 173, The docstring comment still
hardcodes "data/cache/encoder.json" but the cache path is now resolved
dynamically via paths["cache"]; update the docstring in
backend/services/encoder.py (the comment at the Cached at ... line) to reference
the dynamic cache location (e.g. paths["cache"] joined with the encoder
filename) and note it is keyed by the ffmpeg binary fingerprint so the
documentation matches the actual behavior in this module.

Comment on lines +44 to +57
app.get("/api/config/export", async (_req, res) => {
try {
const bundlePath = join(paths.working, `profile-export-${uuidv4()}.zip`);
await mkdir(paths.working, { recursive: true });
const result = await executor.execute<{ bundle: string }>("manage_config", {
action: "export",
bundle_path: bundlePath,
});
const file = result.data?.bundle ?? bundlePath;
if (!existsSync(file)) {
res.status(500).json({ error: "Export failed: bundle not created" });
return;
}
res.download(file, "podcli-profile.zip");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Delete the temporary ZIPs after each request completes.

Both endpoints leave their bundle on disk after the response returns. With the 512MB upload cap, a few imports/exports can leak gigabytes into paths.working/uploadDir and eventually break later requests. Clean the exported file in the res.download() callback and remove req.file.path in a finally block after the import finishes.

Also applies to: 81-97

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/handlers/integrations.routes.ts` around lines 44 - 57, The export route
that calls executor.execute("manage_config", ...) and then res.download(file,
...) must delete the temporary bundle file after the response completes—use the
res.download callback or res.on('finish'/'close') to unlink the bundlePath/file
so the ZIPs don't accumulate; likewise, for the import route that reads
req.file.path, ensure you remove the uploaded file in a finally block after
processing (regardless of success/failure) to guarantee cleanup; reference the
export handler around executor.execute and res.download and the import handler
using req.file.path when adding the unlink logic.

Comment thread src/server.ts
Comment on lines +509 to +515
keep_caption_overlay: z
.boolean()
.optional()
.default(false)
.describe(
"Keep ProRes 4444 alpha caption overlay beside the render (for DaVinci Resolve export). Returns caption_overlay_path and cropped_source_path.",
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Forward keep_caption_overlay through the Web-UI export path.

Both tool schemas accept keep_caption_overlay, but the requests sent to /api/mcp/export never include it. With the Web UI running—which is the preferred path—overlay-enabled exports silently behave like normal renders.

Suggested fix
           body: JSON.stringify({
             video_path: params.video_path,
             clips: [
               {
                 start_second: params.start_second,
                 end_second: params.end_second,
                 title: (params.title || "clip") as string,
                 caption_style: params.caption_style || "hormozi",
                 crop_strategy: params.crop_strategy || "speaker",
                 allow_ass_fallback: params.allow_ass_fallback === true,
+                keep_caption_overlay: params.keep_caption_overlay === true,
                 ...(keepSegments && { segments: keepSegments }),
               },
             ],
             transcript_words: params.transcript_words,
             logo_path: params.logo_path || null,
             outro_path: params.outro_path || null,
+            keep_caption_overlay: params.keep_caption_overlay === true,
           }),
@@
           body: JSON.stringify({
             video_path: resolvedVideoPath,
             clips: resolvedClips,
             transcript_words: resolvedTranscriptWords,
+            keep_caption_overlay: params.keep_caption_overlay === true,
           }),

Also applies to: 603-617, 730-735, 834-838

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server.ts` around lines 509 - 515, The export path in src/server.ts is
not forwarding the keep_caption_overlay flag to the /api/mcp/export endpoint, so
Web-UI exports ignore overlay-enabled renders; update the code that constructs
the export request payload for the Web-UI export path so it includes the
keep_caption_overlay property (preserve its boolean value/default), propagate it
through any intermediate structures or typed interfaces used when calling
/api/mcp/export, and ensure the server handler reading /api/mcp/export consumes
that field; specifically add keep_caption_overlay to the JSON/body sent to
/api/mcp/export wherever export requests are assembled.

Comment on lines +169 to +170
<div class="toggle ${int.enabled ? "on" : ""}" role="switch"
aria-checked="${int.enabled}" data-name="${escapeHtml(int.name)}"></div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a keyboard-accessible control for the integration switch.

The switch is currently a clickable div, which is not keyboard-focusable/operable by default. This blocks non-mouse users from enabling/disabling integrations.

Proposed fix
-            <div class="toggle ${int.enabled ? "on" : ""}" role="switch"
-                 aria-checked="${int.enabled}" data-name="${escapeHtml(int.name)}"></div>
+            <button type="button"
+                 class="toggle ${int.enabled ? "on" : ""}"
+                 role="switch"
+                 aria-checked="${int.enabled}"
+                 aria-label="Toggle ${escapeHtml(int.name)} integration"
+                 data-name="${escapeHtml(int.name)}"></button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="toggle ${int.enabled ? "on" : ""}" role="switch"
aria-checked="${int.enabled}" data-name="${escapeHtml(int.name)}"></div>
<button type="button"
class="toggle ${int.enabled ? "on" : ""}"
role="switch"
aria-checked="${int.enabled}"
aria-label="Toggle ${escapeHtml(int.name)} integration"
data-name="${escapeHtml(int.name)}"></button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/public/integrations.html` around lines 169 - 170, The current
integration toggle is a non-focusable div ("toggle" element with role="switch"
and data-name="${escapeHtml(int.name)}") and must be replaced or augmented with
a keyboard-accessible control: change the element to a semantic, focusable
control (e.g., a <button> or an <input type="checkbox">) that maintains
role="switch" or native semantics, ensure aria-checked reflects int.enabled,
include keyboard event handling (Space/Enter) to toggle the state, keep the
data-name attribute for identification, and ensure visual classes ("on") are
applied when toggled; update any click-only listeners attached to the old
".toggle" to handle keyboard activation as well.

Comment thread src/ui/web-server.ts
Comment on lines 909 to 919
const resolvedPath = path.resolve(filePath);
const isSessionVideo = uiState.videoPath && resolvedPath === path.resolve(uiState.videoPath);
const isUploadedFile = resolvedPath.startsWith(path.resolve(join(paths.working, "uploads")));
const isSessionVideo =
uiState.videoPath && resolvedPath === path.resolve(uiState.videoPath);
const isUploadedFile = resolvedPath.startsWith(
path.resolve(join(paths.working, "uploads")),
);
if (!isSessionVideo && !isUploadedFile) {
res.status(403).json({ error: "Access denied: path not in allowed directories" });
res
.status(403)
.json({ error: "Access denied: path not in allowed directories" });
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tighten the uploaded-file boundary check.

resolvedPath.startsWith(path.resolve(join(paths.working, "uploads"))) also matches sibling paths like .../uploads-backup/foo.mp4, so this new guard can authorize files outside the actual uploads directory.

Suggested fix
   const resolvedPath = path.resolve(filePath);
   const isSessionVideo =
     uiState.videoPath && resolvedPath === path.resolve(uiState.videoPath);
-  const isUploadedFile = resolvedPath.startsWith(
-    path.resolve(join(paths.working, "uploads")),
-  );
+  const uploadsRoot = path.resolve(join(paths.working, "uploads"));
+  const relativeToUploads = path.relative(uploadsRoot, resolvedPath);
+  const isUploadedFile =
+    relativeToUploads !== "" &&
+    !relativeToUploads.startsWith("..") &&
+    !path.isAbsolute(relativeToUploads);
   if (!isSessionVideo && !isUploadedFile) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const resolvedPath = path.resolve(filePath);
const isSessionVideo = uiState.videoPath && resolvedPath === path.resolve(uiState.videoPath);
const isUploadedFile = resolvedPath.startsWith(path.resolve(join(paths.working, "uploads")));
const isSessionVideo =
uiState.videoPath && resolvedPath === path.resolve(uiState.videoPath);
const isUploadedFile = resolvedPath.startsWith(
path.resolve(join(paths.working, "uploads")),
);
if (!isSessionVideo && !isUploadedFile) {
res.status(403).json({ error: "Access denied: path not in allowed directories" });
res
.status(403)
.json({ error: "Access denied: path not in allowed directories" });
return;
const resolvedPath = path.resolve(filePath);
const isSessionVideo =
uiState.videoPath && resolvedPath === path.resolve(uiState.videoPath);
const uploadsRoot = path.resolve(join(paths.working, "uploads"));
const relativeToUploads = path.relative(uploadsRoot, resolvedPath);
const isUploadedFile =
relativeToUploads !== "" &&
!relativeToUploads.startsWith("..") &&
!path.isAbsolute(relativeToUploads);
if (!isSessionVideo && !isUploadedFile) {
res
.status(403)
.json({ error: "Access denied: path not in allowed directories" });
return;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/web-server.ts` around lines 909 - 919, The current isUploadedFile
check uses startsWith on path.resolve(join(paths.working, "uploads")), which
mistakenly matches sibling directories like "uploads-backup"; change the guard
in the web-server logic that computes resolvedPath and isUploadedFile so you
compare against the uploads directory with a directory boundary (e.g., compute
uploadsDir = path.resolve(join(paths.working, "uploads")) and ensure
resolvedPath === uploadsDir or resolvedPath.startsWith(uploadsDir + path.sep))
or use path.relative to verify the file is inside uploads (relative does not
begin with '..'); update the isUploadedFile boolean accordingly where
resolvedPath, isSessionVideo, and paths.working are used.

Comment thread src/ui/web-server.ts
Comment on lines 1643 to 1645
transcriptWordCount: Array.isArray(uiState.transcript?.words)
? uiState.transcript?.words ?? [].length
? (uiState.transcript?.words ?? [].length)
: 0,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Return the transcript word count, not the words array.

This ternary currently yields uiState.transcript.words when it exists, so MCP callers receive an array in transcriptWordCount. src/server.ts treats that field as a number, which breaks transcript detection and workflow guidance.

Suggested fix
     transcriptWordCount: Array.isArray(uiState.transcript?.words)
-      ? (uiState.transcript?.words ?? [].length)
+      ? (uiState.transcript?.words ?? []).length
       : 0,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
transcriptWordCount: Array.isArray(uiState.transcript?.words)
? uiState.transcript?.words ?? [].length
? (uiState.transcript?.words ?? [].length)
: 0,
transcriptWordCount: Array.isArray(uiState.transcript?.words)
? (uiState.transcript?.words ?? []).length
: 0,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/web-server.ts` around lines 1643 - 1645, The field transcriptWordCount
is returning the words array itself due to misplaced parentheses around
(uiState.transcript?.words ?? [].length); change it to return a numeric length
instead by using uiState.transcript?.words.length (or (uiState.transcript?.words
?? []).length) so transcriptWordCount is always a number; update the expression
that sets transcriptWordCount in web-server.ts to reference
uiState.transcript?.words.length and fall back to 0 when missing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant