@openvidstudio/mcp-server 1.3.1 → 1.4.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 (57) hide show
  1. package/dist/canvasExplainer.d.ts +198 -0
  2. package/dist/canvasExplainer.js +661 -0
  3. package/dist/canvasValidate.d.ts +76 -0
  4. package/dist/canvasValidate.js +263 -0
  5. package/dist/config.d.ts +6 -0
  6. package/dist/config.js +2 -0
  7. package/dist/install.d.ts +63 -0
  8. package/dist/install.js +225 -0
  9. package/dist/lessonArc.d.ts +68 -0
  10. package/dist/lessonArc.js +188 -0
  11. package/dist/server.d.ts +1 -1
  12. package/dist/server.js +1 -1
  13. package/dist/stdio.js +12 -4
  14. package/dist/tools/checkScript.d.ts +31 -1
  15. package/dist/tools/checkScript.js +183 -6
  16. package/dist/tools/fitBeats.d.ts +69 -0
  17. package/dist/tools/fitBeats.js +229 -0
  18. package/dist/tools/generateNarration.d.ts +19 -0
  19. package/dist/tools/generateNarration.js +48 -5
  20. package/dist/tools/index.js +6 -0
  21. package/dist/tools/initProject.js +6 -1
  22. package/dist/tools/preflight.js +20 -1
  23. package/dist/tools/renderCanvasExplainer.d.ts +2 -0
  24. package/dist/tools/renderCanvasExplainer.js +56 -0
  25. package/dist/tools/renderVideo.d.ts +3 -0
  26. package/dist/tools/renderVideo.js +13 -4
  27. package/dist/tools/validateBeats.d.ts +2 -0
  28. package/dist/tools/validateBeats.js +5 -5
  29. package/dist/tools/validateCanvasExplainer.d.ts +2 -0
  30. package/dist/tools/validateCanvasExplainer.js +35 -0
  31. package/dist/tools/validateScenes.d.ts +6 -0
  32. package/dist/tools/validateScenes.js +27 -6
  33. package/dist/tools/validateScenesRender.d.ts +8 -0
  34. package/dist/tools/validateScenesRender.js +31 -0
  35. package/dist/tools/writeDescription.d.ts +6 -0
  36. package/dist/tools/writeDescription.js +64 -2
  37. package/docs/API.md +291 -0
  38. package/docs/NARRATION.md +12 -5
  39. package/docs/PIPELINE.md +46 -0
  40. package/docs/SCRIPT.md +63 -3
  41. package/docs/SUPERNOVA.md +53 -15
  42. package/docs/TOOLS.md +4 -1
  43. package/package.json +4 -4
  44. package/templates/default/openvidstudio-core/package.json +9 -9
  45. package/templates/default/openvidstudio-stem/package.json +5 -5
  46. package/templates/default/openvidstudio-stem/src/ComplexityCurve.tsx +108 -0
  47. package/templates/default/openvidstudio-stem/src/Derivation.tsx +334 -0
  48. package/templates/default/openvidstudio-stem/src/Equation.tsx +3 -3
  49. package/templates/default/openvidstudio-stem/src/GraphViz.tsx +249 -0
  50. package/templates/default/openvidstudio-stem/src/LessonCaptions.tsx +135 -0
  51. package/templates/default/openvidstudio-stem/src/MatrixGrid.tsx +187 -0
  52. package/templates/default/openvidstudio-stem/src/Orbit.tsx +241 -0
  53. package/templates/default/openvidstudio-stem/src/TreeViz.tsx +241 -0
  54. package/templates/default/openvidstudio-stem/src/VectorField.tsx +231 -0
  55. package/templates/default/openvidstudio-stem/src/Wave.tsx +234 -0
  56. package/templates/default/openvidstudio-stem/src/index.ts +21 -0
  57. package/templates/default/package.json +10 -10
@@ -0,0 +1,76 @@
1
+ import type { SceneFinding } from "./tools/validateScenes";
2
+ /**
3
+ * validate_canvas_explainer: what validate_scenes does for a Remotion composition, for a canvas explainer. There is
4
+ * no composition to inspect, so the file is loaded in headless Chromium and driven through window.seek(t). The
5
+ * checks are the rules that make phone viewing work: a frame is a pure function of t, text is big enough, nothing
6
+ * sits under the platform's interface, the picture is not blank or still for long, the sound is not a pile-up, and
7
+ * nothing calls Math.random. Findings have the shape validate_scenes returns (beatId, scene, severity, check, message).
8
+ */
9
+ /** The text minimums at the reference canvas (720 on the shorter side), in px. Headlines 44, labels 28, key numbers 64. */
10
+ export declare const TEXT_MINIMUMS: {
11
+ readonly headline: 44;
12
+ readonly label: 28;
13
+ readonly number: 64;
14
+ };
15
+ /** The safe areas of a tall canvas: the top share and the bottom share kept clear. */
16
+ export declare const SAFE_TOP = 0.14;
17
+ export declare const SAFE_BOTTOM = 0.22;
18
+ /** More sources than this playing at once is a pile-up. */
19
+ export declare const MAX_SIMULTANEOUS_SOUNDS = 3;
20
+ export interface CanvasValidateInput {
21
+ projectRoot?: string;
22
+ htmlPath: string;
23
+ width?: number;
24
+ height?: number;
25
+ fps?: number;
26
+ /** A project video: its beats become window.BEATS and its captions are drawn, as in the render. */
27
+ videoName?: string;
28
+ /** Also render the score and judge the sound. Default true. */
29
+ audio?: boolean;
30
+ /** Pixels at the reference size are scaled by min(width, height) / 720 unless this says otherwise. */
31
+ textScale?: number;
32
+ diagramWarnSeconds?: number;
33
+ diagramErrorSeconds?: number;
34
+ staticSeconds?: number;
35
+ }
36
+ export interface CanvasValidateResult {
37
+ /** False when any finding is an error. */
38
+ ok: boolean;
39
+ findings: SceneFinding[];
40
+ durationSeconds: number;
41
+ width: number;
42
+ height: number;
43
+ /** The scale the text minimums were multiplied by. */
44
+ textScale: number;
45
+ framesSampled: number;
46
+ /** Smallest non-caption text drawn, in px at the canvas size, or null when no text was drawn. */
47
+ minTextSize: number | null;
48
+ sound: {
49
+ maxSimultaneous: number;
50
+ peakDb: number | null;
51
+ sources: number;
52
+ } | null;
53
+ elapsedMs: number;
54
+ }
55
+ interface TextRecord {
56
+ text: string;
57
+ size: number;
58
+ x: number;
59
+ y: number;
60
+ caption: boolean;
61
+ }
62
+ interface SourceRecord {
63
+ start: number;
64
+ stop: number | null;
65
+ duration: number | null;
66
+ bufferSeconds: number | null;
67
+ loop: boolean;
68
+ }
69
+ /** Pure: is this text a figure (a number with at most a unit), which is held to the key number minimum when it is drawn big. */
70
+ export declare function isKeyNumber(text: string): boolean;
71
+ /** Pure: the most sources playing at once. A source with no stop and no length plays to `end`. */
72
+ export declare function maxSimultaneous(sources: SourceRecord[], end: number): number;
73
+ /** Pure: the text findings for the texts drawn in the sampled frames. */
74
+ export declare function judgeText(texts: TextRecord[], width: number, height: number, scale: number, scene: string): SceneFinding[];
75
+ export declare function validateCanvasExplainer(input: CanvasValidateInput): Promise<CanvasValidateResult>;
76
+ export {};
@@ -0,0 +1,263 @@
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.MAX_SIMULTANEOUS_SOUNDS = exports.SAFE_BOTTOM = exports.SAFE_TOP = exports.TEXT_MINIMUMS = void 0;
37
+ exports.isKeyNumber = isKeyNumber;
38
+ exports.maxSimultaneous = maxSimultaneous;
39
+ exports.judgeText = judgeText;
40
+ exports.validateCanvasExplainer = validateCanvasExplainer;
41
+ const crypto = __importStar(require("node:crypto"));
42
+ const fs = __importStar(require("node:fs"));
43
+ const os = __importStar(require("node:os"));
44
+ const path = __importStar(require("node:path"));
45
+ const capture_1 = require("@openvidstudio/capture");
46
+ const canvasExplainer_1 = require("./canvasExplainer");
47
+ const lessonArc_1 = require("./lessonArc");
48
+ const util_1 = require("./util");
49
+ /**
50
+ * validate_canvas_explainer: what validate_scenes does for a Remotion composition, for a canvas explainer. There is
51
+ * no composition to inspect, so the file is loaded in headless Chromium and driven through window.seek(t). The
52
+ * checks are the rules that make phone viewing work: a frame is a pure function of t, text is big enough, nothing
53
+ * sits under the platform's interface, the picture is not blank or still for long, the sound is not a pile-up, and
54
+ * nothing calls Math.random. Findings have the shape validate_scenes returns (beatId, scene, severity, check, message).
55
+ */
56
+ /** The text minimums at the reference canvas (720 on the shorter side), in px. Headlines 44, labels 28, key numbers 64. */
57
+ exports.TEXT_MINIMUMS = { headline: 44, label: 28, number: 64 };
58
+ /** Text this much bigger than the label minimum was meant to be a headline or a number, and is held to its minimum. */
59
+ const DISPLAY_FACTOR = 1.2;
60
+ /** The safe areas of a tall canvas: the top share and the bottom share kept clear. */
61
+ exports.SAFE_TOP = 0.14;
62
+ exports.SAFE_BOTTOM = 0.22;
63
+ /** More sources than this playing at once is a pile-up. */
64
+ exports.MAX_SIMULTANEOUS_SOUNDS = 3;
65
+ /** The shortest gap between frames sampled for the arc checks, and the most frames sampled. */
66
+ const MIN_SAMPLE_GAP_SECONDS = 0.5;
67
+ const MAX_SAMPLES = 120;
68
+ const NUMERIC = /^\s*[-+−~]?\s*[$₹€£]?\s*\d[\d.,\s]*\s*(%|[a-zA-Zµ°/²³^]{0,6})\s*$/;
69
+ /** Pure: is this text a figure (a number with at most a unit), which is held to the key number minimum when it is drawn big. */
70
+ function isKeyNumber(text) {
71
+ return NUMERIC.test(text);
72
+ }
73
+ /** Pure: the most sources playing at once. A source with no stop and no length plays to `end`. */
74
+ function maxSimultaneous(sources, end) {
75
+ const events = [];
76
+ for (const s of sources) {
77
+ let stop;
78
+ if (s.stop !== null)
79
+ stop = s.duration !== null ? Math.min(s.stop, s.start + s.duration) : s.stop;
80
+ else if (s.duration !== null)
81
+ stop = s.start + s.duration;
82
+ else if (s.bufferSeconds !== null && !s.loop)
83
+ stop = s.start + s.bufferSeconds;
84
+ else
85
+ stop = end;
86
+ if (stop > s.start) {
87
+ events.push([s.start, 1]);
88
+ events.push([stop, -1]);
89
+ }
90
+ }
91
+ // At the same instant, what ends counts before what begins.
92
+ events.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
93
+ let now = 0;
94
+ let max = 0;
95
+ for (const [, delta] of events) {
96
+ now += delta;
97
+ max = Math.max(max, now);
98
+ }
99
+ return max;
100
+ }
101
+ /** Pure: the text findings for the texts drawn in the sampled frames. */
102
+ function judgeText(texts, width, height, scale, scene) {
103
+ // One finding per distinct text and rule, the worst one kept.
104
+ const found = new Map();
105
+ const add = (key, severity, check, message) => {
106
+ const have = found.get(key);
107
+ if (have && (have.severity === "error" || severity === "warning"))
108
+ return;
109
+ found.set(key, { beatId: "*", scene, severity, message, source: "render", check });
110
+ };
111
+ const label = exports.TEXT_MINIMUMS.label * scale;
112
+ const headline = exports.TEXT_MINIMUMS.headline * scale;
113
+ const number = exports.TEXT_MINIMUMS.number * scale;
114
+ const portrait = height > width;
115
+ const shown = (t) => `"${t.length > 36 ? `${t.slice(0, 36)}...` : t}"`;
116
+ for (const t of texts) {
117
+ if (t.caption || t.text.trim() === "")
118
+ continue;
119
+ const size = Math.round(t.size * 10) / 10;
120
+ const display = t.size >= label * DISPLAY_FACTOR;
121
+ const figure = isKeyNumber(t.text);
122
+ if (display && figure && t.size < number) {
123
+ add(`size:${t.text}`, t.size < number / 2 ? "error" : "warning", "text-size", `The figure ${shown(t.text)} is drawn at ${size}px, under the ${Math.round(number)}px a key number needs at this canvas size (${width}x${height}).`);
124
+ }
125
+ else if (display && !figure && t.size < headline) {
126
+ add(`size:${t.text}`, t.size < headline / 2 ? "error" : "warning", "text-size", `The headline ${shown(t.text)} is drawn at ${size}px, under the ${Math.round(headline)}px a headline needs at this canvas size (${width}x${height}).`);
127
+ }
128
+ else if (!display && t.size < label) {
129
+ add(`size:${t.text}`, t.size < label / 2 ? "error" : "warning", "text-size", `The label ${shown(t.text)} is drawn at ${size}px, under the ${Math.round(label)}px a label needs at this canvas size (${width}x${height}). On a phone it will not be readable.`);
130
+ }
131
+ if (portrait) {
132
+ const top = t.y - t.size * 0.8;
133
+ const bottom = t.y + t.size * 0.2;
134
+ if (top < height * exports.SAFE_TOP) {
135
+ add(`safe:${t.text}`, "warning", "safe-area", `The text ${shown(t.text)} is drawn at y=${Math.round(t.y)}, inside the top ${Math.round(exports.SAFE_TOP * 100)}% of the canvas that the platform's interface covers. Only captions belong there.`);
136
+ }
137
+ else if (bottom > height * (1 - exports.SAFE_BOTTOM)) {
138
+ add(`safe:${t.text}`, "warning", "safe-area", `The text ${shown(t.text)} is drawn at y=${Math.round(t.y)}, inside the bottom ${Math.round(exports.SAFE_BOTTOM * 100)}% of the canvas that captions and the platform's interface use. Only captions belong there.`);
139
+ }
140
+ }
141
+ }
142
+ return Array.from(found.values());
143
+ }
144
+ const hash = (b) => crypto.createHash("sha256").update(b).digest("hex");
145
+ async function validateCanvasExplainer(input) {
146
+ const started = Date.now();
147
+ const projectRoot = (0, util_1.resolveProjectRoot)(input.projectRoot);
148
+ const htmlPath = path.resolve(projectRoot, input.htmlPath);
149
+ if (!fs.existsSync(htmlPath))
150
+ throw new Error(`No explainer at ${htmlPath}.`);
151
+ const width = input.width ?? canvasExplainer_1.DEFAULT_CANVAS_WIDTH;
152
+ const height = input.height ?? canvasExplainer_1.DEFAULT_CANVAS_HEIGHT;
153
+ const fps = input.fps ?? canvasExplainer_1.DEFAULT_CANVAS_FPS;
154
+ const scale = input.textScale ?? Math.min(width, height) / 720;
155
+ const scene = path.basename(htmlPath);
156
+ const audioOn = input.audio !== false;
157
+ const project = input.videoName !== undefined ? (0, canvasExplainer_1.readCanvasProject)(projectRoot, input.videoName) : null;
158
+ const voiceover = project !== null && project.narration === "voice";
159
+ const thresholds = {
160
+ diagramWarnSeconds: input.diagramWarnSeconds ?? lessonArc_1.DEFAULT_ARC_THRESHOLDS.diagramWarnSeconds,
161
+ diagramErrorSeconds: input.diagramErrorSeconds ?? lessonArc_1.DEFAULT_ARC_THRESHOLDS.diagramErrorSeconds,
162
+ staticSeconds: input.staticSeconds ?? lessonArc_1.DEFAULT_ARC_THRESHOLDS.staticSeconds,
163
+ };
164
+ const findings = [];
165
+ const add = (severity, check, message) => findings.push({ beatId: "*", scene, severity, message, source: "render", check });
166
+ for (const w of (0, canvasExplainer_1.sourceWarnings)(fs.readFileSync(htmlPath, "utf8"), htmlPath))
167
+ add("warning", "randomness", w);
168
+ const browser = await (0, capture_1.launchChromium)();
169
+ const scratch = fs.mkdtempSync(path.join(os.tmpdir(), "ovs-canvas-validate-"));
170
+ try {
171
+ const opened = await (0, canvasExplainer_1.openExplainer)(browser, htmlPath, { width, height, fps, voiceover, project, probes: true, requireScore: audioOn });
172
+ const { page, durationSeconds } = opened;
173
+ const texts = [];
174
+ const takeTexts = async () => {
175
+ const got = await page.evaluate(() => {
176
+ const w = window;
177
+ const copy = w.__ovsTexts.slice();
178
+ w.__ovsTexts.length = 0;
179
+ return copy;
180
+ });
181
+ texts.push(...got);
182
+ };
183
+ if (opened.resized)
184
+ add("warning", "canvas-size", `The canvas was not ${width}x${height}. Size it from window.OVS.width and window.OVS.height.`);
185
+ // A frame is a pure function of t: three times, each drawn twice with another time drawn in between.
186
+ const probeTimes = [0.2, 0.5, 0.8].map((f) => f * durationSeconds);
187
+ for (const t of probeTimes) {
188
+ const first = hash(await (0, canvasExplainer_1.captureFrame)(page, t));
189
+ await (0, canvasExplainer_1.captureFrame)(page, (t + durationSeconds * 0.37) % durationSeconds);
190
+ const second = hash(await (0, canvasExplainer_1.captureFrame)(page, t));
191
+ if (first !== second) {
192
+ add("error", "purity", `Drawing t=${t.toFixed(2)}s twice gives two different frames. A frame must be a pure function of t: nothing may depend on how many frames were drawn before it, on the clock, or on a random number.`);
193
+ }
194
+ }
195
+ await takeTexts();
196
+ // Frames across the whole length, for the arc and the text.
197
+ const gap = Math.max(MIN_SAMPLE_GAP_SECONDS, durationSeconds / MAX_SAMPLES);
198
+ const times = [];
199
+ for (let t = 0; t < durationSeconds; t += gap)
200
+ times.push(t);
201
+ const samples = [];
202
+ let previous = null;
203
+ const beatAt = (t) => project?.beats.find((b) => t >= b.start && t < b.end)?.id ?? "*";
204
+ for (const [i, t] of times.entries()) {
205
+ await page.evaluate(() => (window.__ovsCaptionBoxes = []));
206
+ const png = await (0, canvasExplainer_1.captureFrame)(page, t);
207
+ const boxes = (await page.evaluate(() => window.__ovsCaptionBoxes));
208
+ const file = path.join(scratch, `f${i}.png`);
209
+ fs.writeFileSync(file, png);
210
+ const m = await (0, lessonArc_1.measureArcFrame)(file, boxes, previous);
211
+ previous = m.signature;
212
+ samples.push({ time: t, beatId: beatAt(t), drawn: m.drawn, changedFromPrevious: m.changedFromPrevious });
213
+ fs.rmSync(file, { force: true });
214
+ }
215
+ await takeTexts();
216
+ findings.push(...judgeText(texts, width, height, scale, scene));
217
+ for (const f of (0, lessonArc_1.judgeLessonArc)(samples, durationSeconds, thresholds)) {
218
+ findings.push({ beatId: f.beatId, scene, severity: f.severity, message: f.message, source: "render", check: f.check });
219
+ }
220
+ let sound = null;
221
+ if (audioOn) {
222
+ await page.evaluate(() => (window.__ovsSources.length = 0));
223
+ try {
224
+ const score = await (0, canvasExplainer_1.renderScore)(page, durationSeconds, canvasExplainer_1.AUDIO_SAMPLE_RATE, voiceover);
225
+ const sources = (await page.evaluate(() => window.__ovsSources));
226
+ let peak = 0;
227
+ for (let i = 0; i < score.data.length; i++)
228
+ peak = Math.max(peak, Math.abs(score.data[i]));
229
+ const peakDb = peak > 0 ? 20 * Math.log10(peak) : null;
230
+ const most = maxSimultaneous(sources, durationSeconds);
231
+ sound = { maxSimultaneous: most, peakDb: peakDb === null ? null : Math.round(peakDb * 10) / 10, sources: sources.length };
232
+ if (most > exports.MAX_SIMULTANEOUS_SOUNDS) {
233
+ add("warning", "sound-polyphony", `Up to ${most} sounds play at once (a source node each), over ${exports.MAX_SIMULTANEOUS_SOUNDS}. A pile-up of effects is mush under a voice; thin the score.`);
234
+ }
235
+ if (peak >= 1)
236
+ add("warning", "sound-peak", `The score peaks at ${peakDb.toFixed(1)} dBFS, which clips. Lower the master level.`);
237
+ if (peak === 0)
238
+ add("warning", "sound-silent", "The score renders as silence.");
239
+ }
240
+ catch (err) {
241
+ add("error", "sound-kit", err instanceof Error ? err.message : String(err));
242
+ }
243
+ }
244
+ await page.close().catch(() => undefined);
245
+ const own = texts.filter((t) => !t.caption && t.text.trim() !== "");
246
+ return {
247
+ ok: !findings.some((f) => f.severity === "error"),
248
+ findings,
249
+ durationSeconds,
250
+ width,
251
+ height,
252
+ textScale: scale,
253
+ framesSampled: times.length,
254
+ minTextSize: own.length === 0 ? null : Math.round(Math.min(...own.map((t) => t.size)) * 10) / 10,
255
+ sound,
256
+ elapsedMs: Date.now() - started,
257
+ };
258
+ }
259
+ finally {
260
+ await browser.close().catch(() => undefined);
261
+ fs.rmSync(scratch, { recursive: true, force: true });
262
+ }
263
+ }
package/dist/config.d.ts CHANGED
@@ -68,6 +68,11 @@ export interface OpenvidstudioConfig {
68
68
  narration: NarrationConfig;
69
69
  render: RenderConfig;
70
70
  production: ProductionConfig;
71
+ /**
72
+ * Lines write_description appends, each exactly once, at the end of every lesson description ("Constructed
73
+ * animation, not footage", an exam-board non-affiliation line). Absent by default, and then the key is not written.
74
+ */
75
+ disclaimers?: string[];
71
76
  }
72
77
  export interface ConfigPatch {
73
78
  hasHiggsfield?: boolean;
@@ -77,6 +82,7 @@ export interface ConfigPatch {
77
82
  narration?: Partial<NarrationConfig>;
78
83
  render?: Partial<RenderConfig>;
79
84
  production?: Partial<ProductionConfig>;
85
+ disclaimers?: string[];
80
86
  }
81
87
  /**
82
88
  * Defaults chosen so a bare request works with no configuration.
package/dist/config.js CHANGED
@@ -113,6 +113,7 @@ function loadConfig(projectRoot) {
113
113
  creativeLevel: raw.production?.creativeLevel ?? exports.DEFAULT_CONFIG.production.creativeLevel,
114
114
  timeBudgetMinutes: raw.production?.timeBudgetMinutes ?? exports.DEFAULT_CONFIG.production.timeBudgetMinutes,
115
115
  },
116
+ ...(Array.isArray(raw.disclaimers) ? { disclaimers: raw.disclaimers.filter((d) => typeof d === "string") } : {}),
116
117
  };
117
118
  }
118
119
  /**
@@ -149,6 +150,7 @@ function writeConfig(projectRoot, patch) {
149
150
  creativeLevel: patch.production?.creativeLevel ?? existing.production.creativeLevel,
150
151
  timeBudgetMinutes: patch.production?.timeBudgetMinutes ?? existing.production.timeBudgetMinutes,
151
152
  },
153
+ ...((patch.disclaimers ?? existing.disclaimers) !== undefined ? { disclaimers: patch.disclaimers ?? existing.disclaimers } : {}),
152
154
  };
153
155
  fs.mkdirSync(projectRoot, { recursive: true });
154
156
  fs.writeFileSync(configPath(projectRoot), JSON.stringify(merged, null, 2) + "\n", "utf8");
@@ -0,0 +1,63 @@
1
+ /**
2
+ * `npx -y @openvidstudio/mcp-server install <client>`: writes the openvidstudio entry into a client's
3
+ * own MCP config file, so nobody has to hand-edit JSON (and get the Windows `cmd /c` wrapper wrong).
4
+ *
5
+ * It only ever touches the `openvidstudio` key: every other server in the file is left exactly as it
6
+ * was, an unparsable file is never overwritten, and a backup is written before any change.
7
+ *
8
+ * Clients and where their config lives (checked against each vendor's own docs in October 2026):
9
+ * claude-desktop macOS ~/Library/Application Support/Claude/claude_desktop_config.json,
10
+ * Windows %APPDATA%\Claude\claude_desktop_config.json, key "mcpServers"
11
+ * cursor ~/.cursor/mcp.json (global), key "mcpServers"
12
+ * vscode .vscode/mcp.json in the current folder, key "servers", entry has "type": "stdio"
13
+ * claude-code no file: prints the plugin commands (and the `claude mcp add` fallback)
14
+ */
15
+ export declare const INSTALL_CLIENTS: readonly ["claude-desktop", "cursor", "vscode", "claude-code"];
16
+ export type InstallClient = (typeof INSTALL_CLIENTS)[number];
17
+ export interface InstallEnv {
18
+ platform: NodeJS.Platform;
19
+ home: string;
20
+ /** %APPDATA% on Windows; falls back to <home>/AppData/Roaming. */
21
+ appData?: string;
22
+ /** Where `vscode` writes `.vscode/mcp.json`. */
23
+ cwd: string;
24
+ /** Used in the backup file name; injectable so tests are stable. */
25
+ now?: () => Date;
26
+ }
27
+ export interface ServerEntry {
28
+ type?: "stdio";
29
+ command: string;
30
+ args: string[];
31
+ }
32
+ export interface InstallTarget {
33
+ client: InstallClient;
34
+ file: string;
35
+ /** The top-level key the client keeps its servers under. */
36
+ key: "mcpServers" | "servers";
37
+ entry: ServerEntry;
38
+ }
39
+ export interface InstallResult {
40
+ ok: boolean;
41
+ client: InstallClient | null;
42
+ action: "created" | "updated" | "unchanged" | "printed" | "refused" | "dry-run";
43
+ file?: string;
44
+ backup?: string;
45
+ /** Lines to show the user, in order. */
46
+ lines: string[];
47
+ }
48
+ /** The command a client runs to start the server. Windows needs `cmd /c` so `npx` (a .cmd shim) resolves. */
49
+ export declare function serverCommand(platform: NodeJS.Platform): {
50
+ command: string;
51
+ args: string[];
52
+ };
53
+ export declare function installTarget(client: InstallClient, env: InstallEnv): InstallTarget | null;
54
+ /** Pure: merge the openvidstudio entry into the existing file text. Throws if the existing text is not a JSON object. */
55
+ export declare function mergeEntry(existing: string | null, key: "mcpServers" | "servers", entry: ServerEntry): {
56
+ text: string;
57
+ changed: boolean;
58
+ existed: boolean;
59
+ };
60
+ export declare const HELP: string;
61
+ export declare function runInstall(argv: string[], env: InstallEnv): InstallResult;
62
+ /** CLI wrapper used by stdio.ts: runs against the real machine and returns the process exit code. */
63
+ export declare function installCli(argv: string[]): number;
@@ -0,0 +1,225 @@
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.HELP = exports.INSTALL_CLIENTS = void 0;
37
+ exports.serverCommand = serverCommand;
38
+ exports.installTarget = installTarget;
39
+ exports.mergeEntry = mergeEntry;
40
+ exports.runInstall = runInstall;
41
+ exports.installCli = installCli;
42
+ const fs = __importStar(require("node:fs"));
43
+ const os = __importStar(require("node:os"));
44
+ const path = __importStar(require("node:path"));
45
+ /**
46
+ * `npx -y @openvidstudio/mcp-server install <client>`: writes the openvidstudio entry into a client's
47
+ * own MCP config file, so nobody has to hand-edit JSON (and get the Windows `cmd /c` wrapper wrong).
48
+ *
49
+ * It only ever touches the `openvidstudio` key: every other server in the file is left exactly as it
50
+ * was, an unparsable file is never overwritten, and a backup is written before any change.
51
+ *
52
+ * Clients and where their config lives (checked against each vendor's own docs in October 2026):
53
+ * claude-desktop macOS ~/Library/Application Support/Claude/claude_desktop_config.json,
54
+ * Windows %APPDATA%\Claude\claude_desktop_config.json, key "mcpServers"
55
+ * cursor ~/.cursor/mcp.json (global), key "mcpServers"
56
+ * vscode .vscode/mcp.json in the current folder, key "servers", entry has "type": "stdio"
57
+ * claude-code no file: prints the plugin commands (and the `claude mcp add` fallback)
58
+ */
59
+ exports.INSTALL_CLIENTS = ["claude-desktop", "cursor", "vscode", "claude-code"];
60
+ const SERVER_NAME = "openvidstudio";
61
+ const PACKAGE = "@openvidstudio/mcp-server";
62
+ /** The command a client runs to start the server. Windows needs `cmd /c` so `npx` (a .cmd shim) resolves. */
63
+ function serverCommand(platform) {
64
+ const args = ["-y", PACKAGE];
65
+ return platform === "win32" ? { command: "cmd", args: ["/c", "npx", ...args] } : { command: "npx", args };
66
+ }
67
+ function installTarget(client, env) {
68
+ const { command, args } = serverCommand(env.platform);
69
+ switch (client) {
70
+ case "claude-desktop": {
71
+ let file;
72
+ if (env.platform === "darwin")
73
+ file = path.join(env.home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
74
+ else if (env.platform === "win32")
75
+ file = path.join(env.appData ?? path.join(env.home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
76
+ else
77
+ return null; // no official Linux build; the caller prints the block instead
78
+ return { client, file, key: "mcpServers", entry: { command, args } };
79
+ }
80
+ case "cursor":
81
+ return { client, file: path.join(env.home, ".cursor", "mcp.json"), key: "mcpServers", entry: { command, args } };
82
+ case "vscode":
83
+ return { client, file: path.join(env.cwd, ".vscode", "mcp.json"), key: "servers", entry: { type: "stdio", command, args } };
84
+ case "claude-code":
85
+ return null;
86
+ }
87
+ }
88
+ /** Pure: merge the openvidstudio entry into the existing file text. Throws if the existing text is not a JSON object. */
89
+ function mergeEntry(existing, key, entry) {
90
+ let doc = {};
91
+ if (existing !== null && existing.trim() !== "") {
92
+ const parsed = JSON.parse(existing);
93
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
94
+ throw new Error("the file is not a JSON object");
95
+ doc = parsed;
96
+ }
97
+ const servers = (typeof doc[key] === "object" && doc[key] !== null && !Array.isArray(doc[key]) ? doc[key] : {});
98
+ const before = JSON.stringify(servers[SERVER_NAME]);
99
+ const existed = SERVER_NAME in servers;
100
+ servers[SERVER_NAME] = entry;
101
+ doc[key] = servers;
102
+ return { text: JSON.stringify(doc, null, 2) + "\n", changed: before !== JSON.stringify(entry), existed };
103
+ }
104
+ const stamp = (d) => d.toISOString().replace(/[-:]/g, "").replace(/\..*$/, "").replace("T", "-");
105
+ exports.HELP = [
106
+ "openvidstudio-mcp install <client> [--dry-run] [--print]",
107
+ "",
108
+ "Adds the openvidstudio server to a client's MCP config without touching its other servers.",
109
+ "",
110
+ "Clients:",
111
+ " claude-desktop Claude Desktop (Chat tab). Restart Claude afterwards.",
112
+ " cursor Cursor, global config (~/.cursor/mcp.json)",
113
+ " vscode VS Code, this folder (.vscode/mcp.json)",
114
+ " claude-code Prints the plugin commands for Claude Code, the desktop Code tab and Cowork",
115
+ "",
116
+ "Options:",
117
+ " --dry-run show what would be written, write nothing",
118
+ " --print print the config block and exit",
119
+ ].join("\n");
120
+ function runInstall(argv, env) {
121
+ const flags = new Set(argv.filter((a) => a.startsWith("--")));
122
+ const positional = argv.filter((a) => !a.startsWith("--"));
123
+ const client = positional[0];
124
+ if (flags.has("--help") || positional.length === 0) {
125
+ return { ok: positional.length > 0 || flags.has("--help"), client: null, action: "printed", lines: [exports.HELP] };
126
+ }
127
+ if (!client || !exports.INSTALL_CLIENTS.includes(client)) {
128
+ return { ok: false, client: null, action: "refused", lines: [`Unknown client "${positional[0]}". Choose one of: ${exports.INSTALL_CLIENTS.join(", ")}.`, "", exports.HELP] };
129
+ }
130
+ if (client === "claude-code") {
131
+ return {
132
+ ok: true,
133
+ client,
134
+ action: "printed",
135
+ lines: [
136
+ "Claude Code, the Claude desktop Code tab and Cowork: install the plugin (skills and tools together).",
137
+ "",
138
+ " /plugin marketplace add AnayDhawan/openvidstudio",
139
+ " /plugin install openvidstudio@openvidstudio",
140
+ "",
141
+ "From a shell, the same:",
142
+ "",
143
+ " claude plugin marketplace add AnayDhawan/openvidstudio",
144
+ " claude plugin install openvidstudio@openvidstudio",
145
+ "",
146
+ "Tools only, without the plugin:",
147
+ "",
148
+ ` claude mcp add ${SERVER_NAME} -- npx -y ${PACKAGE}`,
149
+ "",
150
+ "Do not keep both: the plugin already registers the server.",
151
+ ],
152
+ };
153
+ }
154
+ const target = installTarget(client, env);
155
+ const { command, args } = serverCommand(env.platform);
156
+ const block = JSON.stringify({ mcpServers: { [SERVER_NAME]: { command, args } } }, null, 2);
157
+ if (!target) {
158
+ return {
159
+ ok: true,
160
+ client,
161
+ action: "printed",
162
+ lines: [
163
+ `${client} has no config file this installer knows on ${env.platform}. Add this block to your client's MCP config and restart it:`,
164
+ "",
165
+ block,
166
+ ],
167
+ };
168
+ }
169
+ if (flags.has("--print")) {
170
+ return { ok: true, client, action: "printed", lines: [JSON.stringify({ [target.key]: { [SERVER_NAME]: target.entry } }, null, 2)] };
171
+ }
172
+ let existing = null;
173
+ if (fs.existsSync(target.file))
174
+ existing = fs.readFileSync(target.file, "utf8");
175
+ let merged;
176
+ try {
177
+ merged = mergeEntry(existing, target.key, target.entry);
178
+ }
179
+ catch (err) {
180
+ return {
181
+ ok: false,
182
+ client,
183
+ action: "refused",
184
+ file: target.file,
185
+ lines: [
186
+ `Not touching ${target.file}: ${err instanceof Error ? err.message : String(err)}.`,
187
+ "Fix or move the file, then run this again. Or paste this block in by hand:",
188
+ "",
189
+ JSON.stringify({ [target.key]: { [SERVER_NAME]: target.entry } }, null, 2),
190
+ ],
191
+ };
192
+ }
193
+ if (!merged.changed && existing !== null) {
194
+ return { ok: true, client, action: "unchanged", file: target.file, lines: [`Already installed in ${target.file}. Nothing changed.`] };
195
+ }
196
+ if (flags.has("--dry-run")) {
197
+ return { ok: true, client, action: "dry-run", file: target.file, lines: [`Would write ${target.file}:`, "", merged.text.trimEnd()] };
198
+ }
199
+ let backup;
200
+ if (existing !== null) {
201
+ backup = `${target.file}.bak-${stamp((env.now ?? (() => new Date()))())}`;
202
+ fs.copyFileSync(target.file, backup);
203
+ }
204
+ fs.mkdirSync(path.dirname(target.file), { recursive: true });
205
+ fs.writeFileSync(target.file, merged.text);
206
+ const lines = [
207
+ `${merged.existed ? "Updated" : "Added"} the ${SERVER_NAME} server in ${target.file}.`,
208
+ ...(backup ? [`Backup of the old file: ${backup}`] : []),
209
+ client === "claude-desktop" ? "Quit Claude Desktop completely and start it again, then check the tools list." : client === "cursor" ? "Restart Cursor (or reload the window), then check the MCP settings." : "Reload VS Code, then check the MCP server list.",
210
+ "Then ask your agent to run preflight; it checks your machine and names the fix for anything missing.",
211
+ ];
212
+ return { ok: true, client, action: existing === null ? "created" : "updated", file: target.file, backup, lines };
213
+ }
214
+ /** CLI wrapper used by stdio.ts: runs against the real machine and returns the process exit code. */
215
+ function installCli(argv) {
216
+ const result = runInstall(argv, {
217
+ platform: process.platform,
218
+ home: os.homedir(),
219
+ appData: process.env.APPDATA,
220
+ cwd: process.cwd(),
221
+ });
222
+ const out = result.lines.join("\n") + "\n";
223
+ (result.ok ? process.stdout : process.stderr).write(out);
224
+ return result.ok ? 0 : 1;
225
+ }