@fullstackcraftllc/codevideo-mcp 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,28 +41,36 @@ npx @fullstackcraftllc/codevideo-cli doctor
41
41
 
42
42
  If Chrome is not detected, run `npx @fullstackcraftllc/codevideo-cli install-browser`. `PATH_TO_CODEVIDEO_CLI` remains available as an optional override for local CLI builds.
43
43
 
44
- Simply drop the following into your `claude_desktop_config.json` file:
44
+ Add the server under `mcpServers` in your `claude_desktop_config.json` file, next to any others you have:
45
45
 
46
46
  ```json
47
47
  {
48
- // ... other MCP servers
49
- "codevideo-mcp": {
50
- "command": "npx",
51
- "args": [
52
- "-y",
53
- "@fullstackcraftllc/codevideo-mcp@latest"
54
- ],
55
- "env": {
56
- "CODEVIDEO_TTS_PROVIDER": "kokoro",
57
- "TTS_SERVICE_URL": "http://localhost:3000"
48
+ "mcpServers": {
49
+ "codevideo-mcp": {
50
+ "command": "npx",
51
+ "args": [
52
+ "-y",
53
+ "@fullstackcraftllc/codevideo-mcp@latest"
54
+ ],
55
+ "env": {
56
+ "CODEVIDEO_TTS_PROVIDER": "kokoro",
57
+ "TTS_SERVICE_URL": "http://localhost:3000"
58
+ }
58
59
  }
59
- },
60
- // ... other MCP servers
60
+ }
61
61
  }
62
62
  ```
63
63
 
64
64
  (Or the ElevenLabs and S3 variables from [Requirements](#requirements) instead of the two Kokoro ones.)
65
65
 
66
+ ## Usage with Claude Code
67
+
68
+ ```shell
69
+ claude mcp add codevideo -e CODEVIDEO_TTS_PROVIDER=kokoro -e TTS_SERVICE_URL=http://localhost:3000 -- npx -y @fullstackcraftllc/codevideo-mcp@latest
70
+ ```
71
+
72
+ Pass the ElevenLabs and S3 variables with `-e` instead of the two Kokoro ones if you use ElevenLabs.
73
+
66
74
  ## Usage with Codex
67
75
 
68
76
  Add the published MCP server to `~/.codex/config.toml`:
@@ -117,6 +125,7 @@ All four video tools take the same `options`:
117
125
  | `keyboardTypingPauseMs`, `standardPauseMs`, `longPauseMs` | pacing |
118
126
  | `voiceId`, `ttsProvider` | narration voice and provider |
119
127
  | `renderMode` | `realtime` (default) or `deterministic`; `renderSeed` and `renderWorkers` (1–32) apply to deterministic renders |
128
+ | `isDiagnosticsVisible` | `true` / `false`: TypeScript squiggles in the editor from the start; lessons can also turn them on and off with `editor-show-diagnostics` and `editor-hide-diagnostics` |
120
129
 
121
130
  They also take `open`: whether to open the finished video in your default player. It defaults to on for single videos and off for whole courses.
122
131
 
@@ -124,7 +133,7 @@ Videos are saved to `CODEVIDEO_OUTPUT_DIR`, or your Desktop.
124
133
 
125
134
  ## Where your data lives
126
135
 
127
- Everything is under `~/.codevideo/`:
136
+ Everything is under `~/.codevideo/`, or the folder `CODEVIDEO_HOME` points to:
128
137
 
129
138
  - `codevideo.db`: courses, lessons, the legacy single slot, and the render queue.
130
139
  - `jobs/<jobId>/`: each render's project, settings and CLI logs (`stdout.log`, `stderr.log`); `jobs/worker.log` is the worker's own log.
@@ -180,3 +189,16 @@ Then, use the following JSON to run the local version on your Claude Desktop:
180
189
 
181
190
  - Compile checks can't resolve third-party packages yet: a lesson that imports `react` reports "Cannot find module 'react'", so courses that depend on npm packages can't pass `codevideo_validate_course`.
182
191
  - With CLI 0.0.13, a project is passed to the CLI on its command line, so a single lesson is limited to about 120 KiB as JSON (about 30 KB on Windows). The MCP refuses bigger ones with a clear message.
192
+
193
+
194
+ ## Lesson verification
195
+
196
+ `codevideo_compile_lesson` freshly replays a lesson from a cloned initial snapshot and checks its saved end-state projects. It reports `outcome: passed | failed | blocked | unsupported`; legacy `status` is `ok` only for passed. Each project reports its root, declared check (`syntax`, `typecheck`, `build`), checked/supporting/unsupported paths, compiler version, structured errors and prerequisite issues. Missing tools/dependencies and nothing-checkable lessons never pass.
197
+
198
+ Dirty editor buffers and disagreement with a stored final snapshot are reported in `lessonIssues`; the server does not autosave or silently repair snapshots. Save buffers using actions, then reapply edits and recheck. Replay warnings remain warnings. Course completion requires passed verification for every mustCompile lesson and the final lesson. Intermediate mustCompile:false lessons remain available for deliberate errors or discussion.
199
+
200
+ TS/JS verification uses real compiler inputs, local project configuration and available dependency declarations. No generated untyped import shims are used. The existing `diagnostics` field and hover checks remain Monaco-compatible presentation checks and do not establish a project pass. Python guarantees syntax only; Go and C# perform offline builds. Rust is explicitly unsupported. See dynamic AST's README for prerequisites and limitations; native builds are not a security sandbox.
201
+
202
+ No dependency restoration from the network occurs during verification. `CODEVIDEO_HOME` optionally sets the MCP state directory (default ~/.codevideo), including for isolated tests.
203
+
204
+ To test unreleased compiler changes without publishing: build dynamic AST and create a tarball with `npm pack --ignore-scripts`; install it locally with `npm install --no-save --package-lock=false <tarball>`, then run the MCP typecheck, tests and build. If the existing local diagnostics changes in codevideo-types are also unreleased, pack that package and set `CODEVIDEO_TYPES_TARBALL` to its absolute path. Set `CODEVIDEO_DYNAMIC_AST_TARBALL` to the compiler tarball path when running `npm run smoke:packed` to exercise the actual packaged compiler through MCP. Neither package version is changed by this workflow. Before shipping, the corresponding compiler release must be published and the MCP dependency/lockfile updated together.
package/dist/index.js CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  // src/index.ts
4
4
  import dotenv from "dotenv";
5
- import fs9 from "node:fs";
5
+ import fs10 from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
8
  // package.json
9
- var version = "1.1.0";
9
+ var version = "1.2.0";
10
10
 
11
11
  // src/index.ts
12
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -89,7 +89,7 @@ var AUTHORING_RULES = `ACTION STYLE AND PACING (this is what makes lessons clear
89
89
  - Never use file-explorer-create-file, file-explorer-create-folder or file-explorer-set-file-contents: they change files with nothing on screen causing it, so things just appear. Don't use the composite-* actions or editor-highlight-code either: they aren't supported yet and do nothing. codevideo_validate_course rejects lessons that use any of these.
90
90
  - A file you just created with the mouse is ALREADY open in the editor \u2014 type into it immediately with editor-type; do NOT reopen it. A file made with touch is not open yet: open it as below. When a lesson continues from the previous one, the file you last edited is already the active editor.
91
91
  - To open a DIFFERENT existing file: mouse-move-file-explorer-file "<filename>" -> file-explorer-open-file "<filename>". IMPORTANT: file-explorer-open-file's value is the FILENAME, not "1" \u2014 using "1" opens a stray editor and your typing is lost.
92
- - Save with editor-save once a file's typing is done (narrating it first): compile checks and file reads see saved contents only.
92
+ - Save with editor-save once a file's typing is done (narrating it first): verification rejects unsaved buffers and checks saved files. After editing, call codevideo_compile_lesson: only outcome passed verifies the declared check. failed means fix the lesson, blocked means prerequisites are unavailable, unsupported means no implemented check. Check each project's check kind: syntax, typecheck, or build. Monaco diagnostics reproduce presentation only; they do not prove a project builds. Verification never installs dependencies; author required manifests/configuration and use available dependency declarations or self-contained examples.
93
93
  - editor-type's value is the literal text to insert (including newlines).
94
94
 
95
95
  NARRATION:
@@ -280,7 +280,7 @@ import os from "node:os";
280
280
  import path from "node:path";
281
281
  import { createRequire } from "node:module";
282
282
  var codevideoHome = () => {
283
- const dir = path.join(os.homedir(), ".codevideo");
283
+ const dir = process.env.CODEVIDEO_HOME || path.join(os.homedir(), ".codevideo");
284
284
  fs.mkdirSync(dir, { recursive: true });
285
285
  return dir;
286
286
  };
@@ -1012,6 +1012,7 @@ var RenderOptionsSchema = z.object({
1012
1012
  isEditorTabsVisible: z.boolean().optional(),
1013
1013
  isMinimapVisible: z.boolean().optional(),
1014
1014
  isMouseVisible: z.boolean().optional().describe("Draw the mouse cursor. Mouse actions keep their timing and effects when it's hidden."),
1015
+ isDiagnosticsVisible: z.boolean().optional().describe("Show TypeScript squiggles (red errors, yellow warnings) for the whole video. editor-show-diagnostics / editor-hide-diagnostics actions switch them for part of a lesson instead."),
1015
1016
  wordWrap: z.enum(["on", "off"]).optional(),
1016
1017
  keyboardTypingPauseMs: z.number().int().positive().optional(),
1017
1018
  standardPauseMs: z.number().int().positive().optional(),
@@ -1037,6 +1038,7 @@ var IDE_PROP_KEYS = [
1037
1038
  "isEditorTabsVisible",
1038
1039
  "isMinimapVisible",
1039
1040
  "isMouseVisible",
1041
+ "isDiagnosticsVisible",
1040
1042
  "wordWrap",
1041
1043
  "keyboardTypingPauseMs",
1042
1044
  "standardPauseMs",
@@ -1545,8 +1547,8 @@ var validateActions = (actions) => {
1545
1547
  };
1546
1548
 
1547
1549
  // src/tools/courseTools.ts
1548
- import fs7 from "node:fs";
1549
- import path7 from "node:path";
1550
+ import fs8 from "node:fs";
1551
+ import path8 from "node:path";
1550
1552
  import { isValidAction as isValidAction2 } from "@fullstackcraftllc/codevideo-types";
1551
1553
 
1552
1554
  // src/store/courseStore.ts
@@ -1852,41 +1854,181 @@ var getLessonFile = (courseId, lessonId, filePath) => {
1852
1854
  const entry = fileEntriesFromSnapshot(lesson.finalSnapshot).find((f) => samePath(f.path, filePath));
1853
1855
  return entry ? entry.content : null;
1854
1856
  };
1855
- var getLessonFiles = (courseId, lessonId) => {
1856
- const lesson = getLesson(courseId, lessonId);
1857
- return fileEntriesFromSnapshot(lesson.finalSnapshot);
1858
- };
1857
+
1858
+ // src/tools/verifyLesson.ts
1859
+ import { isDeepStrictEqual } from "node:util";
1860
+ import { VirtualIDE as VirtualIDE3 } from "@fullstackcraftllc/codevideo-virtual-ide";
1859
1861
 
1860
1862
  // src/tools/compileCheck.ts
1863
+ import { createRequire as createRequire3 } from "node:module";
1864
+
1865
+ // src/tools/diagnostics.ts
1861
1866
  import { createRequire as createRequire2 } from "node:module";
1862
- var cachedCompileProject = null;
1863
- var getCompileProject = () => {
1864
- if (!cachedCompileProject) {
1865
- const require2 = createRequire2(import.meta.url);
1866
- cachedCompileProject = require2("@fullstackcraftllc/codevideo-dynamic-ast").compileProject;
1867
+ import fs7 from "node:fs";
1868
+ import path7 from "node:path";
1869
+ import {
1870
+ DIAGNOSTICS_TYPESCRIPT_VERSION,
1871
+ createDiagnosticsShims,
1872
+ getDiagnosticsChecks,
1873
+ getDiagnosticsCompilerOptions,
1874
+ isDiagnosticsProgramFile,
1875
+ normalizeDiagnosticsPath,
1876
+ toCodeVideoDiagnostic,
1877
+ toDiagnosticsFileName
1878
+ } from "@fullstackcraftllc/codevideo-types";
1879
+ var cachedTypeScript = null;
1880
+ var loadTypeScript = () => {
1881
+ if (!cachedTypeScript) {
1882
+ const ts = createRequire2(import.meta.url)("typescript-5.4.5");
1883
+ if (ts.version !== DIAGNOSTICS_TYPESCRIPT_VERSION) {
1884
+ throw new Error(`Diagnostics need TypeScript ${DIAGNOSTICS_TYPESCRIPT_VERSION} (the renderer's), found ${ts.version}`);
1885
+ }
1886
+ cachedTypeScript = ts;
1887
+ }
1888
+ return cachedTypeScript;
1889
+ };
1890
+ var isDiagnosticsFile = (filePath) => isDiagnosticsProgramFile(normalizeDiagnosticsPath(filePath));
1891
+ var computeDiagnostics = (files, targets) => {
1892
+ const ts = loadTypeScript();
1893
+ const project = files.map((file) => ({ path: normalizeDiagnosticsPath(file.path), content: file.content }));
1894
+ const program = [...project.filter((file) => isDiagnosticsProgramFile(file.path)), createDiagnosticsShims(project)];
1895
+ const sources = new Map(program.map((file) => [toDiagnosticsFileName(file.path), file.content]));
1896
+ const options = getDiagnosticsCompilerOptions();
1897
+ const libDirectory = path7.dirname(ts.getDefaultLibFilePath(options));
1898
+ const isLib = (fileName) => fileName.startsWith(libDirectory) && fs7.existsSync(fileName);
1899
+ const read = (fileName) => sources.get(fileName) ?? (isLib(fileName) ? fs7.readFileSync(fileName, "utf8") : void 0);
1900
+ const host = {
1901
+ getScriptFileNames: () => [...sources.keys()],
1902
+ getScriptVersion: () => "1",
1903
+ getScriptSnapshot: (fileName) => {
1904
+ const text = read(fileName);
1905
+ return text === void 0 ? void 0 : ts.ScriptSnapshot.fromString(text);
1906
+ },
1907
+ getCurrentDirectory: () => "",
1908
+ getCompilationSettings: () => options,
1909
+ getDefaultLibFileName: (settings) => ts.getDefaultLibFilePath(settings),
1910
+ fileExists: (fileName) => sources.has(fileName) || isLib(fileName),
1911
+ readFile: read
1912
+ };
1913
+ const service = ts.createLanguageService(host, ts.createDocumentRegistry());
1914
+ try {
1915
+ const contents = new Map(project.map((file) => [file.path, file.content]));
1916
+ const wanted = [...new Set((targets ?? project.map((file) => file.path)).map(normalizeDiagnosticsPath))].sort();
1917
+ const diagnostics = [];
1918
+ for (const target of wanted) {
1919
+ const checks = getDiagnosticsChecks(target), content = contents.get(target);
1920
+ if (!checks || content === void 0) continue;
1921
+ const fileName = toDiagnosticsFileName(target);
1922
+ const raw = [
1923
+ ...checks.syntactic ? service.getSyntacticDiagnostics(fileName) : [],
1924
+ ...checks.semantic ? service.getSemanticDiagnostics(fileName) : []
1925
+ ];
1926
+ diagnostics.push(...raw.map((diagnostic) => toCodeVideoDiagnostic(target, content, diagnostic)));
1927
+ }
1928
+ return diagnostics;
1929
+ } finally {
1930
+ service.dispose();
1867
1931
  }
1868
- return cachedCompileProject;
1869
1932
  };
1933
+
1934
+ // src/tools/compileCheck.ts
1935
+ var compiler;
1870
1936
  var compileFiles = async (files) => {
1871
- if (files.length === 0) {
1872
- return { status: "ok", projects: [], message: "No files to compile." };
1937
+ let result;
1938
+ try {
1939
+ compiler ??= createRequire3(import.meta.url)("@fullstackcraftllc/codevideo-dynamic-ast").compileProject;
1940
+ result = await compiler(files);
1941
+ if (!result.outcome) throw new Error("Installed dynamic-ast lacks explicit verification outcomes; install the updated package.");
1942
+ } catch (error) {
1943
+ result = { outcome: "blocked", projects: [{ language: "unknown", root: ".", check: "syntax", outcome: "blocked", errors: [], issues: [{ kind: "runner", message: String(error) }], checkedPaths: [], supportingPaths: [], unsupportedPaths: [] }] };
1873
1944
  }
1945
+ const response = { ...result, status: result.outcome === "passed" ? "ok" : "errors" };
1946
+ if (files.some((f) => isDiagnosticsFile(f.path))) {
1947
+ try {
1948
+ response.diagnostics = computeDiagnostics(files);
1949
+ } catch (error) {
1950
+ response.presentationIssues = [String(error)];
1951
+ }
1952
+ }
1953
+ return response;
1954
+ };
1955
+
1956
+ // src/tools/verifyLesson.ts
1957
+ var verifyLesson = async (lesson) => {
1958
+ const issues = [];
1959
+ const warnings = [];
1960
+ const ide = new VirtualIDE3();
1961
+ let index = -1;
1962
+ let snapshot;
1874
1963
  try {
1875
- const compileProject = getCompileProject();
1876
- const result = await compileProject(files);
1877
- const projects = result.projects.map((p) => ({
1878
- language: p.language,
1879
- errors: p.errors
1880
- }));
1881
- const hasErrors = projects.some((p) => p.errors.length > 0);
1882
- return { status: hasErrors ? "errors" : "ok", projects };
1964
+ if (lesson.initialSnapshot) ide.applyCourseSnapshot(structuredClone(lesson.initialSnapshot));
1965
+ let seen = 0;
1966
+ for (index = 0; index < lesson.actions.length; index++) {
1967
+ ide.applyAction(lesson.actions[index]);
1968
+ const logs = ide.getLogs();
1969
+ for (const log2 of logs.slice(seen)) {
1970
+ if (log2.type === "error") issues.push({ kind: "replay", actionIndex: index, message: log2.message });
1971
+ if (log2.type === "warning") warnings.push({ actionIndex: index, message: log2.message });
1972
+ }
1973
+ seen = logs.length;
1974
+ }
1975
+ snapshot = ide.getCourseSnapshot();
1883
1976
  } catch (error) {
1884
- const message = error instanceof Error ? error.message : String(error);
1885
- return {
1886
- status: "errors",
1887
- projects: [{ language: "unknown", errors: [{ file: "unknown", line: 0, message: `Compile check failed to run: ${message}` }] }]
1888
- };
1977
+ issues.push({ kind: "replay", actionIndex: index < 0 ? void 0 : Math.min(index, lesson.actions.length - 1), message: String(error) });
1978
+ return { status: "errors", outcome: "failed", projects: [], fileCount: 0, lessonIssues: issues, warnings };
1979
+ }
1980
+ if (lesson.finalSnapshot && !isDeepStrictEqual(snapshot, lesson.finalSnapshot)) issues.push({ kind: "snapshot", message: "Stored final snapshot disagrees with initial snapshot plus actions. Reapply the lesson actions to recompute the snapshot chain." });
1981
+ for (const editor of snapshot.editorSnapshot.editors) {
1982
+ if (!editor.isSaved) issues.push({ kind: "unsaved", path: editor.filename, message: `Unsaved buffer ${editor.filename}: add an editor-save action before the lesson ends.` });
1889
1983
  }
1984
+ const files = ide.getFullFilePathsAndContents();
1985
+ const compiled = await compileFiles(files);
1986
+ const outcome = issues.length ? "failed" : compiled.outcome;
1987
+ return { ...compiled, status: outcome === "passed" ? "ok" : "errors", outcome, fileCount: files.length, lessonIssues: issues, warnings };
1988
+ };
1989
+
1990
+ // src/validation/diagnosticHovers.ts
1991
+ import { VirtualIDE as VirtualIDE4 } from "@fullstackcraftllc/codevideo-virtual-ide";
1992
+ import {
1993
+ diagnosticContains,
1994
+ normalizeDiagnosticsPath as normalizeDiagnosticsPath2,
1995
+ parseDiagnosticLocation
1996
+ } from "@fullstackcraftllc/codevideo-types";
1997
+ var filesOnScreen = (ide) => {
1998
+ const files = new Map(ide.getFullFilePathsAndContents().map((file) => [normalizeDiagnosticsPath2(file.path), file.content]));
1999
+ for (const editor of ide.getEditorSnapshot().editors) files.set(normalizeDiagnosticsPath2(editor.filename), editor.content);
2000
+ return [...files].map(([path10, content]) => ({ path: path10, content }));
2001
+ };
2002
+ var checkDiagnosticHovers = (initialSnapshot, actions) => {
2003
+ if (!actions.some((action) => action.name === "mouse-move-editor-diagnostic")) return [];
2004
+ const ide = new VirtualIDE4();
2005
+ ide.applyCourseSnapshot(structuredClone(initialSnapshot));
2006
+ const problems = [];
2007
+ actions.forEach((action, index) => {
2008
+ if (action.name === "mouse-move-editor-diagnostic") {
2009
+ const location = parseDiagnosticLocation(action.value);
2010
+ const active = ide.getEditorSnapshot().editors.find((editor) => editor.isActive);
2011
+ if (!location) {
2012
+ problems.push({ index, value: action.value, reason: "the value isn't a 'path:line:column' location" });
2013
+ } else if (!active || normalizeDiagnosticsPath2(active.filename) !== location.path) {
2014
+ problems.push({ index, value: action.value, reason: `${location.path} isn't the active editor here (active: ${active?.filename ?? "none"})` });
2015
+ } else {
2016
+ const diagnostics = computeDiagnostics(filesOnScreen(ide), [location.path]);
2017
+ const column = location.column;
2018
+ const hit = column === void 0 ? diagnostics.some((d) => d.line === location.line) : diagnostics.some((d) => diagnosticContains(d, location.line, column));
2019
+ if (!hit) {
2020
+ const there = diagnostics.map((d) => `${d.line}:${d.column}-${d.endLine}:${d.endColumn} "${d.message.split("\n")[0]}"`);
2021
+ problems.push({
2022
+ index,
2023
+ value: action.value,
2024
+ reason: there.length ? `no squiggle at that spot; ${location.path} has ${there.join("; ")}` : `${location.path} has no squiggles at this point in the lesson`
2025
+ });
2026
+ }
2027
+ }
2028
+ }
2029
+ ide.applyAction(action);
2030
+ });
2031
+ return problems;
1890
2032
  };
1891
2033
 
1892
2034
  // src/tools/courseTools.ts
@@ -1899,8 +2041,8 @@ var filesChangedBy = (lesson) => {
1899
2041
  const before = new Map(fileEntriesFromSnapshot(lesson.initialSnapshot).map((f) => [f.path, f.content]));
1900
2042
  const open = new Set((lesson.finalSnapshot.editorSnapshot?.editors ?? []).map((e) => e.filename));
1901
2043
  return fileEntriesFromSnapshot(lesson.finalSnapshot).filter((f) => before.get(f.path) !== f.content).slice(0, 10).map((f) => {
1902
- const path9 = f.path.replace(/^\/+/, "");
1903
- return { path: path9, change: before.has(f.path) ? "changed" : "created", open: open.has(path9) };
2044
+ const path10 = f.path.replace(/^\/+/, "");
2045
+ return { path: path10, change: before.has(f.path) ? "changed" : "created", open: open.has(path10) };
1904
2046
  });
1905
2047
  };
1906
2048
  var addLessonTool = (courseId, input) => {
@@ -1950,9 +2092,8 @@ var getLessonFileTool = (courseId, lessonId, filePath) => {
1950
2092
  return ok({ courseId, lessonId, path: filePath, found: true, content });
1951
2093
  };
1952
2094
  var compileLessonTool = async (courseId, lessonId) => {
1953
- const files = getLessonFiles(courseId, lessonId);
1954
- const result = await compileFiles(files);
1955
- return ok({ courseId, lessonId, fileCount: files.length, ...result });
2095
+ const result = await verifyLesson(getLesson(courseId, lessonId));
2096
+ return ok({ courseId, lessonId, ...result });
1956
2097
  };
1957
2098
  var validateCourse = async (courseId) => {
1958
2099
  const lessons = listLessons(courseId);
@@ -1971,11 +2112,13 @@ var validateCourse = async (courseId) => {
1971
2112
  }
1972
2113
  let compileStatus = "skipped";
1973
2114
  let compileErrors = [];
2115
+ let verification;
1974
2116
  if (lesson.mustCompile) {
1975
- const c = await compileFiles(getLessonFiles(courseId, lesson.id));
2117
+ const c = verification = await verifyLesson(lesson);
1976
2118
  compileStatus = c.status;
1977
- compileErrors = c.projects.filter((p) => p.errors.length > 0);
2119
+ compileErrors = c.projects.filter((p) => p.outcome !== "passed");
1978
2120
  }
2121
+ const diagnosticHoverProblems = checkDiagnosticHovers(lesson.initialSnapshot, lesson.actions);
1979
2122
  results.push({
1980
2123
  lessonId: lesson.id,
1981
2124
  name: lesson.name,
@@ -1988,7 +2131,9 @@ var validateCourse = async (courseId) => {
1988
2131
  snapshotContinuous,
1989
2132
  mustCompile: lesson.mustCompile,
1990
2133
  compileStatus,
1991
- compileErrors
2134
+ compileErrors,
2135
+ verification,
2136
+ diagnosticHoverProblems
1992
2137
  });
1993
2138
  }
1994
2139
  const allActionsValid = results.every((r) => r.actionsValid);
@@ -2000,7 +2145,8 @@ var validateCourse = async (courseId) => {
2000
2145
  const allMustCompileClean = compileEnforced.every((r) => r.compileStatus === "ok");
2001
2146
  const finalLesson = results[results.length - 1];
2002
2147
  const finalCompilesClean = !!finalLesson && finalLesson.mustCompile && finalLesson.compileStatus === "ok";
2003
- const done = lessons.length > 0 && allHaveActions && allActionsValid && allAllowed && allOpenWithSpeech && allContinuous && allMustCompileClean && finalCompilesClean;
2148
+ const allHoversLand = results.every((r) => r.diagnosticHoverProblems.length === 0);
2149
+ const done = lessons.length > 0 && allHaveActions && allActionsValid && allAllowed && allOpenWithSpeech && allContinuous && allMustCompileClean && finalCompilesClean && allHoversLand;
2004
2150
  const reasons = [];
2005
2151
  if (lessons.length === 0) reasons.push("Course has no lessons.");
2006
2152
  for (const lesson of results.filter((result) => !result.hasActions)) {
@@ -2021,8 +2167,16 @@ var validateCourse = async (courseId) => {
2021
2167
  for (const result of results.filter((r) => r.rejectedActions.length > 0)) {
2022
2168
  reasons.push(`Lesson "${result.name}" (${result.lessonId}) uses actions lessons can't use; ${describeRejected(result.rejectedActions)}.`);
2023
2169
  }
2170
+ for (const result of results.filter((r) => r.diagnosticHoverProblems.length > 0)) {
2171
+ const details = result.diagnosticHoverProblems.map((p) => `action ${p.index} (${p.value}): ${p.reason}`);
2172
+ reasons.push(`Lesson "${result.name}" (${result.lessonId}) has mouse-move-editor-diagnostic actions that miss: ${details.join("; ")}.`);
2173
+ }
2024
2174
  if (!allContinuous) reasons.push("Some lessons do not follow cleanly from the previous lesson (snapshot discontinuity).");
2025
- if (!allMustCompileClean) reasons.push("Some lessons marked mustCompile do not compile cleanly.");
2175
+ for (const result of results.filter((r) => r.mustCompile && r.verification?.outcome !== "passed")) {
2176
+ const verification = result.verification;
2177
+ const detail = [...verification.lessonIssues.map((i) => i.message), ...verification.projects.flatMap((p) => p.issues.map((i) => i.message))].join("; ");
2178
+ reasons.push(`Lesson "${result.name}" verification ${verification.outcome}: ${detail}`);
2179
+ }
2026
2180
  if (finalLesson && !finalLesson.mustCompile) reasons.push("The final lesson must be marked mustCompile (all code should work by the end).");
2027
2181
  else if (!finalCompilesClean) reasons.push("The final lesson does not compile cleanly.");
2028
2182
  return { courseId, done, lessonCount: lessons.length, lessons: results, reasons };
@@ -2050,11 +2204,11 @@ var getCourseStatusTool = (courseId) => {
2050
2204
  };
2051
2205
  var exportCourseTool = (courseId) => {
2052
2206
  const course = exportCourse(courseId);
2053
- const dir = path7.join(codevideoHome(), "exports");
2054
- fs7.mkdirSync(dir, { recursive: true });
2055
- const file = path7.join(dir, `${courseId.replace(/[^a-z0-9-_]+/gi, "_")}.json`);
2207
+ const dir = path8.join(codevideoHome(), "exports");
2208
+ fs8.mkdirSync(dir, { recursive: true });
2209
+ const file = path8.join(dir, `${courseId.replace(/[^a-z0-9-_]+/gi, "_")}.json`);
2056
2210
  const json = JSON.stringify(course, null, 2);
2057
- fs7.writeFileSync(file, json);
2211
+ fs8.writeFileSync(file, json);
2058
2212
  return ok({
2059
2213
  courseId,
2060
2214
  path: file,
@@ -2066,8 +2220,8 @@ var exportCourseTool = (courseId) => {
2066
2220
 
2067
2221
  // src/tools/renderTools.ts
2068
2222
  import os4 from "os";
2069
- import fs8 from "fs";
2070
- import path8 from "path";
2223
+ import fs9 from "fs";
2224
+ import path9 from "path";
2071
2225
  import { generateHtmlStringFromLesson } from "@fullstackcraftllc/codevideo-exporters";
2072
2226
  var ok2 = (data) => JSON.stringify(data, null, 2);
2073
2227
  var toILesson = (l) => ({
@@ -2080,10 +2234,10 @@ var toILesson = (l) => ({
2080
2234
  });
2081
2235
  var writePreviewHtml = async (courseId, l) => {
2082
2236
  const html = await generateHtmlStringFromLesson(toILesson(l), {});
2083
- const outDir = path8.join(os4.homedir(), ".codevideo", "previews", courseId);
2084
- fs8.mkdirSync(outDir, { recursive: true });
2085
- const file = path8.join(outDir, `${l.id}.html`);
2086
- fs8.writeFileSync(file, html, "utf8");
2237
+ const outDir = path9.join(os4.homedir(), ".codevideo", "previews", courseId);
2238
+ fs9.mkdirSync(outDir, { recursive: true });
2239
+ const file = path9.join(outDir, `${l.id}.html`);
2240
+ fs9.writeFileSync(file, html, "utf8");
2087
2241
  return file;
2088
2242
  };
2089
2243
  var generateLessonPreviewTool = async (courseId, lessonId) => {
@@ -3122,8 +3276,9 @@ function setupMcpServerTools(server) {
3122
3276
  const tool = server.tool.bind(server);
3123
3277
  tool(
3124
3278
  "codevideo_instructions_to_create_a_course",
3279
+ "Instructions for building a multi-lesson course as one JSON object passed between tool calls (the older flow: codevideo_create_course_with_initial_metadata, then codevideo_add_lesson_to_course per lesson). For new courses prefer the server-side flow in codevideo_instructions_to_build_course_from_scratch.",
3125
3280
  {
3126
- additionalContext: z2.string()
3281
+ additionalContext: z2.string().describe("What to build, in your own words: the lessons for a course, or the topic for a lesson.")
3127
3282
  },
3128
3283
  async ({ additionalContext }) => {
3129
3284
  return {
@@ -3136,8 +3291,9 @@ function setupMcpServerTools(server) {
3136
3291
  );
3137
3292
  tool(
3138
3293
  "codevideo_instructions_to_create_lesson_actions",
3294
+ "Instructions for writing the actions of a single lesson: the action reference, the authoring rules, and how to store and render the result.",
3139
3295
  {
3140
- additionalContext: z2.string()
3296
+ additionalContext: z2.string().describe("What to build, in your own words: the lessons for a course, or the topic for a lesson.")
3141
3297
  },
3142
3298
  async ({ additionalContext }) => {
3143
3299
  return {
@@ -3150,8 +3306,9 @@ function setupMcpServerTools(server) {
3150
3306
  );
3151
3307
  tool(
3152
3308
  "codevideo_instructions_to_create_lesson_video",
3309
+ "Instructions for writing a single lesson and rendering it to video, either from an actions array (codevideo_make_video_from_actions) or from a full lesson object with snapshots (codevideo_make_video_from_lesson).",
3153
3310
  {
3154
- additionalContext: z2.string()
3311
+ additionalContext: z2.string().describe("What to build, in your own words: the lessons for a course, or the topic for a lesson.")
3155
3312
  },
3156
3313
  async ({ additionalContext }) => {
3157
3314
  return {
@@ -3181,6 +3338,7 @@ function setupMcpServerTools(server) {
3181
3338
  );
3182
3339
  tool(
3183
3340
  "codevideo_get_empty_lesson_snapshot",
3341
+ "An empty course snapshot (no files, editors or terminal output) as JSON, to use as the initialSnapshot of a lesson that starts from nothing.",
3184
3342
  {},
3185
3343
  async () => {
3186
3344
  return {
@@ -3193,6 +3351,7 @@ function setupMcpServerTools(server) {
3193
3351
  );
3194
3352
  tool(
3195
3353
  "codevideo_create_course_with_initial_metadata",
3354
+ "Returns a new course object as JSON (id, name, description, primaryLanguage, no lessons) for the older flow where the course JSON is passed between tool calls. Nothing is stored; the server-side flow uses codevideo_create_course.",
3196
3355
  {
3197
3356
  id: z2.string(),
3198
3357
  name: z2.string(),
@@ -3210,6 +3369,7 @@ function setupMcpServerTools(server) {
3210
3369
  );
3211
3370
  tool(
3212
3371
  "codevideo_add_actions_to_lesson",
3372
+ "Sets a lesson object's actions, replacing any it had, and returns the updated lesson as JSON. Nothing is stored; for stored courses use codevideo_set_lesson_actions.",
3213
3373
  {
3214
3374
  lesson: LessonSchema,
3215
3375
  actions: z2.array(ActionSchema)
@@ -3243,6 +3403,7 @@ function setupMcpServerTools(server) {
3243
3403
  );
3244
3404
  tool(
3245
3405
  "codevideo_add_lesson_to_course",
3406
+ "Appends a new, empty lesson (id, name, description, an empty initial snapshot) to a course object and returns the updated course as JSON. Nothing is stored; for stored courses use codevideo_add_lesson.",
3246
3407
  {
3247
3408
  course: CourseSchema,
3248
3409
  id: z2.string(),
@@ -3274,8 +3435,9 @@ function setupMcpServerTools(server) {
3274
3435
  );
3275
3436
  tool(
3276
3437
  "codevideo_get_example_actions_array_by_keyword",
3438
+ "An example actions array as JSON for a keyword: javascript, js, console log or log; go, golang, golang console log or golang log; python, python console log or python log. Returns an empty string when nothing matches.",
3277
3439
  {
3278
- keyword: z2.string()
3440
+ keyword: z2.string().describe("One of the keywords listed in this tool's description.")
3279
3441
  },
3280
3442
  async ({ keyword }) => {
3281
3443
  return {
@@ -3288,8 +3450,9 @@ function setupMcpServerTools(server) {
3288
3450
  );
3289
3451
  tool(
3290
3452
  "codevideo_get_example_lesson_by_keyword",
3453
+ "An example lesson as JSON (a Go CLI tool explored from an existing codebase) for the keyword go, golang, explore codebase, existing codebase or go cli tool. Returns an empty string when nothing matches.",
3291
3454
  {
3292
- keyword: z2.string()
3455
+ keyword: z2.string().describe("One of the keywords listed in this tool's description.")
3293
3456
  },
3294
3457
  async ({ keyword }) => {
3295
3458
  return {
@@ -3428,6 +3591,7 @@ function setupMcpServerTools(server) {
3428
3591
  );
3429
3592
  tool(
3430
3593
  "codevideo_is_repeatable_action",
3594
+ "Whether an action's value is a repeat count (how many times to press the key or run the step) rather than text. Returns true or false.",
3431
3595
  {
3432
3596
  action: ActionSchema
3433
3597
  },
@@ -3642,13 +3806,13 @@ function setupMcpServerTools(server) {
3642
3806
  "codevideo_get_lesson_file",
3643
3807
  "Get the contents of ONE file at the END of a lesson (cheap targeted read instead of dumping the whole snapshot).",
3644
3808
  { courseId: z2.string(), lessonId: z2.string(), path: z2.string() },
3645
- async ({ courseId, lessonId, path: path9 }) => ({
3646
- content: [{ type: "text", text: getLessonFileTool(courseId, lessonId, path9) }]
3809
+ async ({ courseId, lessonId, path: path10 }) => ({
3810
+ content: [{ type: "text", text: getLessonFileTool(courseId, lessonId, path10) }]
3647
3811
  })
3648
3812
  );
3649
3813
  tool(
3650
3814
  "codevideo_compile_lesson",
3651
- "Compile the code at the end of a lesson and return only the structured errors. Use this to check a lesson builds before moving on.",
3815
+ "Replay a lesson and verify its saved end-state projects offline. Returns passed, failed, blocked, or unsupported, with check kinds and actionable issues. Monaco diagnostics are presentation-only; dirty buffers prevent a pass.",
3652
3816
  { courseId: z2.string(), lessonId: z2.string() },
3653
3817
  async ({ courseId, lessonId }) => ({
3654
3818
  content: [{ type: "text", text: await compileLessonTool(courseId, lessonId) }]
@@ -3656,7 +3820,7 @@ function setupMcpServerTools(server) {
3656
3820
  );
3657
3821
  tool(
3658
3822
  "codevideo_validate_course",
3659
- "Validate the whole course: per-lesson action validity, snapshot continuity, and compilation (for mustCompile lessons + the final lesson). Returns done:true when the course is complete.",
3823
+ "Validate actions, continuity, diagnostic hovers, and fresh saved-file project verification. Every mustCompile lesson and the final lesson must have outcome passed; blocked/unsupported/dirty/stale states prevent done:true.",
3660
3824
  { courseId: z2.string() },
3661
3825
  async ({ courseId }) => ({
3662
3826
  content: [{ type: "text", text: await validateCourseTool(courseId) }]
@@ -3758,7 +3922,7 @@ function setupMcpServerTools(server) {
3758
3922
  );
3759
3923
  }
3760
3924
  var invokedPath = process.argv[1];
3761
- var isDirectExecution = invokedPath !== void 0 && fs9.realpathSync(fileURLToPath(import.meta.url)) === fs9.realpathSync(invokedPath);
3925
+ var isDirectExecution = invokedPath !== void 0 && fs10.realpathSync(fileURLToPath(import.meta.url)) === fs10.realpathSync(invokedPath);
3762
3926
  if (isDirectExecution && process.argv.includes("--render-worker")) {
3763
3927
  runWorker().then(
3764
3928
  () => process.exit(0),