@openvidstudio/mcp-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/beats.d.ts +47 -0
  2. package/dist/beats.js +9 -0
  3. package/dist/capture.d.ts +116 -0
  4. package/dist/capture.js +148 -0
  5. package/dist/config.d.ts +58 -0
  6. package/dist/config.js +130 -0
  7. package/dist/paths.d.ts +20 -0
  8. package/dist/paths.js +57 -0
  9. package/dist/scenes/pngSize.d.ts +11 -0
  10. package/dist/scenes/pngSize.js +75 -0
  11. package/dist/scenes/templates.d.ts +42 -0
  12. package/dist/scenes/templates.js +468 -0
  13. package/dist/server.d.ts +16 -0
  14. package/dist/server.js +37 -0
  15. package/dist/stdio.d.ts +2 -0
  16. package/dist/stdio.js +33 -0
  17. package/dist/tools/captureScreenRecording.d.ts +28 -0
  18. package/dist/tools/captureScreenRecording.js +181 -0
  19. package/dist/tools/captureScreenshot.d.ts +34 -0
  20. package/dist/tools/captureScreenshot.js +148 -0
  21. package/dist/tools/contactSheet.d.ts +36 -0
  22. package/dist/tools/contactSheet.js +116 -0
  23. package/dist/tools/generateNarration.d.ts +54 -0
  24. package/dist/tools/generateNarration.js +185 -0
  25. package/dist/tools/importHiggsfieldClip.d.ts +41 -0
  26. package/dist/tools/importHiggsfieldClip.js +108 -0
  27. package/dist/tools/index.d.ts +13 -0
  28. package/dist/tools/index.js +47 -0
  29. package/dist/tools/initProject.d.ts +37 -0
  30. package/dist/tools/initProject.js +166 -0
  31. package/dist/tools/mcp.d.ts +33 -0
  32. package/dist/tools/mcp.js +36 -0
  33. package/dist/tools/planBeats.d.ts +40 -0
  34. package/dist/tools/planBeats.js +143 -0
  35. package/dist/tools/preflight.d.ts +30 -0
  36. package/dist/tools/preflight.js +170 -0
  37. package/dist/tools/qcExtractFrames.d.ts +25 -0
  38. package/dist/tools/qcExtractFrames.js +129 -0
  39. package/dist/tools/renderVideo.d.ts +43 -0
  40. package/dist/tools/renderVideo.js +120 -0
  41. package/dist/tools/scaffoldScene.d.ts +23 -0
  42. package/dist/tools/scaffoldScene.js +147 -0
  43. package/dist/tools/stitchComposition.d.ts +24 -0
  44. package/dist/tools/stitchComposition.js +230 -0
  45. package/dist/tools/validateBeats.d.ts +14 -0
  46. package/dist/tools/validateBeats.js +164 -0
  47. package/dist/tools/validateScenes.d.ts +31 -0
  48. package/dist/tools/validateScenes.js +191 -0
  49. package/dist/tools/writeBeatsFile.d.ts +20 -0
  50. package/dist/tools/writeBeatsFile.js +81 -0
  51. package/dist/util.d.ts +43 -0
  52. package/dist/util.js +163 -0
  53. package/docs/API.md +344 -0
  54. package/docs/CAPTURE.md +106 -0
  55. package/docs/HIGGSFIELD.md +118 -0
  56. package/docs/NARRATION.md +111 -0
  57. package/docs/OVERVIEW.md +112 -0
  58. package/docs/PIPELINE.md +173 -0
  59. package/docs/PLANNING.md +206 -0
  60. package/docs/SCRIPT.md +50 -0
  61. package/docs/STYLE.md +88 -0
  62. package/package.json +54 -0
  63. package/templates/default/CLAUDE.md +63 -0
  64. package/templates/default/openvidstudio-core/package.json +51 -0
  65. package/templates/default/openvidstudio-core/scripts/gen-sfx.sh +74 -0
  66. package/templates/default/openvidstudio-core/src/ChecklistPanel.tsx +97 -0
  67. package/templates/default/openvidstudio-core/src/CinematicScene.tsx +53 -0
  68. package/templates/default/openvidstudio-core/src/CodePanel.tsx +122 -0
  69. package/templates/default/openvidstudio-core/src/CursorActor.tsx +90 -0
  70. package/templates/default/openvidstudio-core/src/DeviceFrame.tsx +74 -0
  71. package/templates/default/openvidstudio-core/src/KineticText.tsx +142 -0
  72. package/templates/default/openvidstudio-core/src/RepoCta.tsx +128 -0
  73. package/templates/default/openvidstudio-core/src/TerminalReplay.tsx +263 -0
  74. package/templates/default/openvidstudio-core/src/camera.tsx +141 -0
  75. package/templates/default/openvidstudio-core/src/index.ts +60 -0
  76. package/templates/default/openvidstudio-core/src/look.tsx +90 -0
  77. package/templates/default/openvidstudio-core/src/motion.ts +50 -0
  78. package/templates/default/openvidstudio-core/src/sfx.tsx +106 -0
  79. package/templates/default/openvidstudio-core/src/tokens.ts +41 -0
  80. package/templates/default/package.json +31 -0
  81. package/templates/default/public/sfx/bell.wav +0 -0
  82. package/templates/default/public/sfx/blip.wav +0 -0
  83. package/templates/default/public/sfx/click.wav +0 -0
  84. package/templates/default/public/sfx/key_a.wav +0 -0
  85. package/templates/default/public/sfx/key_b.wav +0 -0
  86. package/templates/default/public/sfx/key_c.wav +0 -0
  87. package/templates/default/public/sfx/key_enter.wav +0 -0
  88. package/templates/default/public/sfx/music-bed.mp3 +0 -0
  89. package/templates/default/public/sfx/success.wav +0 -0
  90. package/templates/default/public/sfx/whoosh.wav +0 -0
  91. package/templates/default/remotion.config.ts +13 -0
  92. package/templates/default/src/Root.tsx +9 -0
  93. package/templates/default/src/index.ts +4 -0
  94. package/templates/default/tsconfig.json +16 -0
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateBeatsLogic = validateBeatsLogic;
4
+ exports.registerValidateBeats = registerValidateBeats;
5
+ const zod_1 = require("zod");
6
+ const capture_1 = require("../capture");
7
+ const mcp_1 = require("./mcp");
8
+ const EM_DASH = "—";
9
+ const MIN_PACE = 2.3;
10
+ const MAX_PACE = 2.9;
11
+ const CAPTURE_METHODS = ["screenshot", "recording", "dom-demo", "higgsfield"];
12
+ function wordCount(text) {
13
+ const trimmed = text.trim();
14
+ return trimmed.length === 0 ? 0 : trimmed.split(/\s+/).length;
15
+ }
16
+ /**
17
+ * Mechanically enforces packages/docs/PLANNING.md §4 (captureMethod field spec) and
18
+ * packages/docs/SCRIPT.md (VO pacing, no em dashes). Takes the drafted beats.json
19
+ * content directly (not yet written to disk, not assumed to already be valid), and
20
+ * returns every failure found rather than stopping at the first -- this is the tool's
21
+ * normal "no" answer, never a thrown exception.
22
+ */
23
+ function validateBeatsLogic(beatsJson) {
24
+ const errors = [];
25
+ if (typeof beatsJson !== "object" || beatsJson === null) {
26
+ return { valid: false, errors: ["beatsJson must be an object"] };
27
+ }
28
+ const root = beatsJson;
29
+ const fps = typeof root.fps === "number" && root.fps > 0 ? root.fps : 30;
30
+ if (!Array.isArray(root.beats)) {
31
+ return { valid: false, errors: ["beatsJson.beats must be an array"] };
32
+ }
33
+ const beats = root.beats;
34
+ if (beats.length === 0) {
35
+ errors.push("beats array is empty");
36
+ }
37
+ const seenIds = new Set();
38
+ let expectedStart = 0;
39
+ beats.forEach((raw, i) => {
40
+ if (typeof raw !== "object" || raw === null) {
41
+ errors.push(`beat[${i}]: is not an object`);
42
+ return;
43
+ }
44
+ const beat = raw;
45
+ const label = typeof beat.id === "string" && beat.id.length > 0 ? `beat "${beat.id}"` : `beat[${i}]`;
46
+ if (typeof beat.id !== "string" || beat.id.length === 0) {
47
+ errors.push(`${label}: missing or invalid "id"`);
48
+ }
49
+ else if (seenIds.has(beat.id)) {
50
+ errors.push(`${label}: duplicate beat id`);
51
+ }
52
+ else {
53
+ seenIds.add(beat.id);
54
+ }
55
+ const start = beat.start;
56
+ const duration = beat.duration;
57
+ const hasStart = typeof start === "number";
58
+ const hasDuration = typeof duration === "number" && duration > 0;
59
+ if (!hasStart) {
60
+ errors.push(`${label}: missing or invalid "start"`);
61
+ }
62
+ else if (start !== expectedStart) {
63
+ errors.push(`${label}: "start" (${start}) must equal ${expectedStart} (contiguous timing: the first beat starts ` +
64
+ `at 0, every later beat starts at the previous beat's start + duration)`);
65
+ }
66
+ if (!hasDuration) {
67
+ errors.push(`${label}: missing or invalid "duration" (must be a positive number)`);
68
+ }
69
+ if (hasStart && hasDuration) {
70
+ expectedStart = start + duration;
71
+ }
72
+ else if (hasDuration) {
73
+ expectedStart += duration;
74
+ }
75
+ const vo = beat.vo;
76
+ if (typeof vo !== "string" || vo.length === 0) {
77
+ errors.push(`${label}: missing or invalid "vo"`);
78
+ }
79
+ else {
80
+ if (vo.includes(EM_DASH)) {
81
+ errors.push(`${label}: "vo" contains an em dash, rewrite with commas/periods`);
82
+ }
83
+ if (hasDuration) {
84
+ const seconds = duration / fps;
85
+ if (seconds > 0) {
86
+ const words = wordCount(vo);
87
+ const pace = words / seconds;
88
+ if (pace < MIN_PACE || pace > MAX_PACE) {
89
+ errors.push(`${label}: VO pace ${pace.toFixed(2)} words/sec is outside the ${MIN_PACE}-${MAX_PACE} words/sec ` +
90
+ `budget (${words} words over ${seconds.toFixed(2)}s, per SCRIPT.md)`);
91
+ }
92
+ }
93
+ }
94
+ }
95
+ const visual = beat.visual;
96
+ if (typeof visual !== "object" || visual === null) {
97
+ errors.push(`${label}: missing or invalid "visual"`);
98
+ return;
99
+ }
100
+ const v = visual;
101
+ for (const [key, val] of Object.entries(v)) {
102
+ if (typeof val === "string" && val.includes(EM_DASH)) {
103
+ errors.push(`${label}: visual.${key} contains an em dash, rewrite with commas/periods`);
104
+ }
105
+ }
106
+ const captureMethod = v.captureMethod;
107
+ if (typeof captureMethod !== "string" || !CAPTURE_METHODS.includes(captureMethod)) {
108
+ errors.push(`${label}: visual.captureMethod must be one of ${CAPTURE_METHODS.map((m) => `"${m}"`).join(", ")}`);
109
+ return;
110
+ }
111
+ if (captureMethod === "screenshot" || captureMethod === "recording") {
112
+ if (typeof v.url !== "string" || v.url.length === 0) {
113
+ errors.push(`${label}: visual.url is required for captureMethod "${captureMethod}"`);
114
+ }
115
+ if (!Array.isArray(v.interactions)) {
116
+ errors.push(`${label}: visual.interactions (array, may be empty) is required for captureMethod "${captureMethod}"`);
117
+ }
118
+ else {
119
+ // Validate each interaction against the exact zod schema capture_screenshot /
120
+ // capture_screen_recording enforce at replay time (interactionSchema, from
121
+ // capture.ts), so a schema violation (e.g. a "navigate" or "type" step, which
122
+ // this pipeline does not support) surfaces here at draft time, not at capture
123
+ // time.
124
+ v.interactions.forEach((interaction, idx) => {
125
+ const parsed = capture_1.interactionSchema.safeParse(interaction);
126
+ if (!parsed.success) {
127
+ const issues = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`);
128
+ errors.push(`${label}: visual.interactions[${idx}] is invalid: ${issues.join("; ")}`);
129
+ }
130
+ });
131
+ }
132
+ }
133
+ else if (captureMethod === "higgsfield") {
134
+ if (typeof v.higgsfieldPrompt !== "string" || v.higgsfieldPrompt.length === 0) {
135
+ errors.push(`${label}: visual.higgsfieldPrompt is required for captureMethod "higgsfield"`);
136
+ }
137
+ }
138
+ // dom-demo: no additional required fields.
139
+ });
140
+ return { valid: errors.length === 0, errors };
141
+ }
142
+ function registerValidateBeats(server) {
143
+ server.registerTool("validate_beats", {
144
+ title: "Validate a drafted beats.json",
145
+ description: "Mechanically checks a drafted beats.json (an object, not a file path -- it hasn't been written to " +
146
+ "disk yet) against PLANNING.md's captureMethod field spec and SCRIPT.md's pacing rules: contiguous " +
147
+ "frame timing (30fps unless the file states its own fps; the first beat starts at 0, every later " +
148
+ "beat starts at the previous beat's start + duration), no em dashes in vo/visual string fields, VO " +
149
+ "word-count vs the 2.3-2.9 words/sec budget checked in both directions (too many words for the " +
150
+ "duration and suspiciously few), every beat has a captureMethod, and method-specific required " +
151
+ "fields (url + interactions for screenshot/recording, higgsfieldPrompt for higgsfield, nothing extra " +
152
+ "for dom-demo). For screenshot/recording beats, every interactions[] entry is also validated against " +
153
+ "the exact schema capture_screenshot/capture_screen_recording enforce at replay time (click, fill, " +
154
+ "select, hover, scroll, wait), so a shape mismatch fails here at draft time instead of at capture " +
155
+ "time. Returns { valid, errors } with every failure found, never just the first -- this is " +
156
+ "the tool's normal answer for an invalid draft, not an exceptional case, so it never throws for a " +
157
+ "validation failure.",
158
+ inputSchema: {
159
+ beatsJson: zod_1.z
160
+ .unknown()
161
+ .describe("The drafted beats.json content (an object), not a file path."),
162
+ },
163
+ }, async ({ beatsJson }) => (0, mcp_1.runTool)("validate_beats", () => validateBeatsLogic(beatsJson)));
164
+ }
@@ -0,0 +1,31 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /**
3
+ * The worst failure class in this pipeline is silent wrongness: a scene renders
4
+ * successfully and is simply wrong.
5
+ *
6
+ * The commonest instance is framing. Scenes lay content out on a 1920x1080 stage, but
7
+ * the camera only ever shows stage divided by scale, so at 1.4 that is 1371x771 and
8
+ * anything wider is cropped at the frame edge. Nothing errors. Building one real video
9
+ * this cropped three scenes, and it was only caught by rendering stills and looking.
10
+ *
11
+ * This reads the scene sources and reports the mismatches that are visible statically:
12
+ * camera scales against the content widths declared in the file, missing capture
13
+ * assets, and scenes that never move.
14
+ */
15
+ export interface ValidateScenesInput {
16
+ projectRoot?: string;
17
+ videoName: string;
18
+ }
19
+ export interface SceneFinding {
20
+ beatId: string;
21
+ scene: string;
22
+ severity: "error" | "warning";
23
+ message: string;
24
+ }
25
+ export interface ValidateScenesResult {
26
+ ok: boolean;
27
+ scenesChecked: number;
28
+ findings: SceneFinding[];
29
+ }
30
+ export declare function runValidateScenes(input: ValidateScenesInput): ValidateScenesResult;
31
+ export declare function registerValidateScenes(server: McpServer): void;
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.runValidateScenes = runValidateScenes;
37
+ exports.registerValidateScenes = registerValidateScenes;
38
+ const fs = __importStar(require("node:fs"));
39
+ const path = __importStar(require("node:path"));
40
+ const zod_1 = require("zod");
41
+ const util_1 = require("../util");
42
+ const config_1 = require("../config");
43
+ const mcp_1 = require("./mcp");
44
+ const STAGE_W = 1920;
45
+ const STAGE_H = 1080;
46
+ /** Every numeric `scale:` in the camera array. */
47
+ function cameraScales(src) {
48
+ return [...src.matchAll(/scale:\s*([\d.]+)/g)]
49
+ .map((m) => Number.parseFloat(m[1]))
50
+ .filter((n) => Number.isFinite(n));
51
+ }
52
+ /** Explicit pixel widths a scene declares, which is what gets cropped. */
53
+ function declaredWidths(src) {
54
+ const out = [];
55
+ for (const m of src.matchAll(/\bwidth[=:]\s*\{?\s*(\d{3,4})\b/g))
56
+ out.push(Number(m[1]));
57
+ for (const m of src.matchAll(/\bFRAME_W\s*=\s*(\d{3,4})\b/g))
58
+ out.push(Number(m[1]));
59
+ for (const m of src.matchAll(/\bmaxWidth:\s*(\d{3,4})\b/g))
60
+ out.push(Number(m[1]));
61
+ return out;
62
+ }
63
+ function runValidateScenes(input) {
64
+ const videoName = (0, util_1.sanitizeSegment)(input.videoName, "videoName");
65
+ const projectRoot = (0, util_1.resolveProjectRoot)(input.projectRoot);
66
+ const config = (0, config_1.loadConfig)(projectRoot);
67
+ const videoDir = path.join(projectRoot, "src", "videos", videoName);
68
+ const scenesDir = path.join(videoDir, "scenes");
69
+ const beatsFile = path.join(videoDir, "beats.json");
70
+ if (!fs.existsSync(beatsFile)) {
71
+ throw new Error(`No beats.json at ${beatsFile}. Run write_beats_file first.`);
72
+ }
73
+ const doc = JSON.parse(fs.readFileSync(beatsFile, "utf8"));
74
+ const findings = [];
75
+ let checked = 0;
76
+ for (const beat of doc.beats) {
77
+ const component = (0, util_1.pascalCase)(beat.id);
78
+ const file = path.join(scenesDir, `${component}.tsx`);
79
+ if (!fs.existsSync(file)) {
80
+ findings.push({
81
+ beatId: beat.id,
82
+ scene: `${component}.tsx`,
83
+ severity: "error",
84
+ message: `No scene file. Run scaffold_scene for this beat.`,
85
+ });
86
+ continue;
87
+ }
88
+ checked++;
89
+ const src = fs.readFileSync(file, "utf8");
90
+ // Framing. The binding number is the scene's highest scale, not its first.
91
+ const scales = cameraScales(src);
92
+ const maxScale = scales.length ? Math.max(...scales) : 1;
93
+ const visibleW = Math.round(STAGE_W / maxScale);
94
+ const visibleH = Math.round(STAGE_H / maxScale);
95
+ for (const w of declaredWidths(src)) {
96
+ if (w > visibleW) {
97
+ findings.push({
98
+ beatId: beat.id,
99
+ scene: `${component}.tsx`,
100
+ severity: "error",
101
+ message: `Content declares ${w}px wide but the camera reaches scale ${maxScale}, which shows only ` +
102
+ `${visibleW}x${visibleH} of the stage. It will be cropped at the frame edge and the render ` +
103
+ `will not warn. Narrow the content or lower the camera scale.`,
104
+ });
105
+ }
106
+ }
107
+ // Motion. A frozen frame is a QC fail in STYLE.md.
108
+ if (scales.length >= 2 && new Set(scales).size === 1) {
109
+ const positions = new Set([...src.matchAll(/\bx:\s*(\d+),\s*y:\s*(\d+)/g)].map((m) => `${m[1]},${m[2]}`));
110
+ if (positions.size <= 1) {
111
+ findings.push({
112
+ beatId: beat.id,
113
+ scene: `${component}.tsx`,
114
+ severity: "warning",
115
+ message: `Camera never moves. STYLE.md fails a static frame; give it at least a subtle push.`,
116
+ });
117
+ }
118
+ }
119
+ if (scales.length < 2) {
120
+ findings.push({
121
+ beatId: beat.id,
122
+ scene: `${component}.tsx`,
123
+ severity: "warning",
124
+ message: `Fewer than two camera keyframes, so nothing moves.`,
125
+ });
126
+ }
127
+ // Capture assets the scene references but that are not on disk yet.
128
+ const method = beat.visual?.captureMethod;
129
+ if (method === "screenshot") {
130
+ const asset = path.join(projectRoot, "public", "images", `${beat.id}.png`);
131
+ if (!fs.existsSync(asset)) {
132
+ findings.push({
133
+ beatId: beat.id,
134
+ scene: `${component}.tsx`,
135
+ severity: "error",
136
+ message: `References public/images/${beat.id}.png, which does not exist. Run capture_screenshot.`,
137
+ });
138
+ }
139
+ }
140
+ if (method === "recording" || method === "higgsfield") {
141
+ const asset = path.join(projectRoot, "public", "video", `${beat.id}.mp4`);
142
+ if (!fs.existsSync(asset)) {
143
+ findings.push({
144
+ beatId: beat.id,
145
+ scene: `${component}.tsx`,
146
+ severity: "error",
147
+ message: `References public/video/${beat.id}.mp4, which does not exist.`,
148
+ });
149
+ }
150
+ }
151
+ if (/TODO/.test(src)) {
152
+ findings.push({
153
+ beatId: beat.id,
154
+ scene: `${component}.tsx`,
155
+ severity: "warning",
156
+ message: `Still contains a TODO.`,
157
+ });
158
+ }
159
+ }
160
+ // Narration, which stitch_composition skips silently when absent.
161
+ for (const beat of doc.beats) {
162
+ const vo = path.join(projectRoot, "public", "audio", "vo", `${beat.id}.mp3`);
163
+ if (!fs.existsSync(vo)) {
164
+ findings.push({
165
+ beatId: beat.id,
166
+ scene: "-",
167
+ severity: "warning",
168
+ message: `No narration at public/audio/vo/${beat.id}.mp3. stitch_composition skips this silently, so the ` +
169
+ `beat renders with no voice and nothing reports it. Run generate_narration.`,
170
+ });
171
+ }
172
+ }
173
+ void config;
174
+ return { ok: !findings.some((f) => f.severity === "error"), scenesChecked: checked, findings };
175
+ }
176
+ function registerValidateScenes(server) {
177
+ server.registerTool("validate_scenes", {
178
+ title: "Check scenes for problems that render without erroring",
179
+ description: "Reads every scene for a video and reports what would otherwise only be caught by watching the " +
180
+ "output. Chiefly framing: content is laid out on a 1920x1080 stage but the camera shows stage " +
181
+ "divided by scale, so a panel wider than the visible box is cropped at the frame edge with no " +
182
+ "warning at all. Also flags scenes whose camera never moves (a static frame fails STYLE.md), " +
183
+ "capture assets a scene references that are not on disk, leftover TODOs, and beats with no " +
184
+ "narration file, since stitch_composition skips those silently and the beat renders mute. Run it " +
185
+ "after scaffolding and again before rendering.",
186
+ inputSchema: {
187
+ projectRoot: zod_1.z.string().optional(),
188
+ videoName: zod_1.z.string().min(1),
189
+ },
190
+ }, async (input) => (0, mcp_1.runTool)("validate_scenes", () => runValidateScenes(input)));
191
+ }
@@ -0,0 +1,20 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { type ValidateBeatsResult } from "./validateBeats";
3
+ export interface WriteBeatsFileInput {
4
+ projectRoot?: string;
5
+ videoName: string;
6
+ beatsJson: unknown;
7
+ }
8
+ export type WriteBeatsFileResult = {
9
+ written: true;
10
+ path: string;
11
+ } | ValidateBeatsResult;
12
+ /**
13
+ * Runs validate_beats' own logic directly (no MCP round-trip) and refuses to write
14
+ * anything invalid. This is the mechanical half of PLANNING.md's "approval gate": it
15
+ * enforces schema validity, not the human-approval step itself (the calling agent
16
+ * must show the dev the full drafted beats.json and get explicit approval before ever
17
+ * calling this tool -- see the tool description below).
18
+ */
19
+ export declare function runWriteBeatsFile(input: WriteBeatsFileInput): WriteBeatsFileResult;
20
+ export declare function registerWriteBeatsFile(server: McpServer): void;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.runWriteBeatsFile = runWriteBeatsFile;
37
+ exports.registerWriteBeatsFile = registerWriteBeatsFile;
38
+ const fs = __importStar(require("node:fs"));
39
+ const path = __importStar(require("node:path"));
40
+ const zod_1 = require("zod");
41
+ const validateBeats_1 = require("./validateBeats");
42
+ const util_1 = require("../util");
43
+ const mcp_1 = require("./mcp");
44
+ /**
45
+ * Runs validate_beats' own logic directly (no MCP round-trip) and refuses to write
46
+ * anything invalid. This is the mechanical half of PLANNING.md's "approval gate": it
47
+ * enforces schema validity, not the human-approval step itself (the calling agent
48
+ * must show the dev the full drafted beats.json and get explicit approval before ever
49
+ * calling this tool -- see the tool description below).
50
+ */
51
+ function runWriteBeatsFile(input) {
52
+ const validation = (0, validateBeats_1.validateBeatsLogic)(input.beatsJson);
53
+ if (!validation.valid) {
54
+ return validation;
55
+ }
56
+ const videoName = (0, util_1.sanitizeSegment)(input.videoName, "videoName");
57
+ const projectRoot = (0, util_1.resolveProjectRoot)(input.projectRoot);
58
+ const videoDir = path.join(projectRoot, "src", "videos", videoName);
59
+ fs.mkdirSync(videoDir, { recursive: true });
60
+ const filePath = path.join(videoDir, "beats.json");
61
+ fs.writeFileSync(filePath, JSON.stringify(input.beatsJson, null, 2) + "\n", "utf8");
62
+ return { written: true, path: filePath };
63
+ }
64
+ function registerWriteBeatsFile(server) {
65
+ server.registerTool("write_beats_file", {
66
+ title: "Write a validated beats.json to disk",
67
+ description: "Runs validate_beats' own validation logic internally first (a direct function call, not an MCP " +
68
+ "round-trip) and refuses to write anything invalid, returning the same { valid: false, errors } " +
69
+ "shape instead. On success writes src/videos/<videoName>/beats.json (pretty-printed, 2-space indent) " +
70
+ "and returns { written: true, path }. IMPORTANT: this tool only enforces schema validity. It does " +
71
+ "NOT enforce PLANNING.md's mandatory human-approval gate -- the calling agent must show the dev the " +
72
+ "full drafted beats.json (not a summary) and get explicit approval before ever calling this tool, " +
73
+ "even for a small, obviously-fine-looking draft. Passing this tool's validation is necessary but " +
74
+ "not sufficient; it is not a substitute for that approval step.",
75
+ inputSchema: {
76
+ projectRoot: zod_1.z.string().optional(),
77
+ videoName: zod_1.z.string().min(1),
78
+ beatsJson: zod_1.z.unknown().describe("The drafted beats.json content (an object), not a file path."),
79
+ },
80
+ }, async (input) => (0, mcp_1.runTool)("write_beats_file", () => runWriteBeatsFile(input)));
81
+ }
package/dist/util.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ export declare function resolveProjectRoot(projectRoot?: string): string;
2
+ /**
3
+ * Validates a value used as a single path segment (project/video name, beat id)
4
+ * before it's joined into any filesystem path or process argv. Rejects `..`,
5
+ * `/`, `\`, and anything else outside a conservative safe set.
6
+ */
7
+ export declare function sanitizeSegment(value: string, label: string): string;
8
+ /**
9
+ * PascalCases an id for use as a component/file name: split on any run of
10
+ * non-alphanumeric characters, capitalize the first letter of each remaining
11
+ * segment, join. A result starting with a digit gets an "S" prefix (invalid
12
+ * JS identifier otherwise); an empty result (all-symbol id) falls back to "Scene".
13
+ * scaffold_scene and stitch_composition both call this, so a beat's scene file name
14
+ * and its import in the generated composition always agree.
15
+ */
16
+ export declare function pascalCase(id: string): string;
17
+ /**
18
+ * Validates a path argument that will be handed to a spawned child process
19
+ * (npx/ffmpeg): no shell metacharacters, and must resolve inside `root` so a
20
+ * crafted `../../` can't point the command at an arbitrary filesystem location.
21
+ */
22
+ export declare function sanitizeRelativeOutPath(root: string, relPath: string, label: string): string;
23
+ /**
24
+ * Copies a directory tree, skipping any `node_modules` subtree and never
25
+ * overwriting a file that already exists at the destination -- used by
26
+ * init_project to lay the bundled template shell into a user's project without
27
+ * clobbering anything the dev (or a prior init_project call, for a second video
28
+ * in the same project) has already touched.
29
+ */
30
+ export declare function copyTemplateTree(src: string, dest: string): void;
31
+ export interface SpawnResult {
32
+ code: number | null;
33
+ stdout: string;
34
+ stderr: string;
35
+ }
36
+ /**
37
+ * Runs a child process to completion and captures its stdout/stderr in memory.
38
+ * `spawn` with an argv array, never `exec`/`execSync` with an interpolated shell string --
39
+ * render_video and qc_extract_frames both go through this. Capturing the child's output
40
+ * here has nothing to do with *this* process's own stdout (the MCP transport); it's
41
+ * returned as normal tool-result content by the caller.
42
+ */
43
+ export declare function spawnCapture(command: string, args: string[], cwd: string): Promise<SpawnResult>;