@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 +36 -14
- package/dist/index.js +225 -61
- package/dist/index.js.map +4 -4
- package/package.json +6 -5
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
5
|
+
import fs10 from "node:fs";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
8
|
// package.json
|
|
9
|
-
var version = "1.
|
|
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):
|
|
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
|
|
1549
|
-
import
|
|
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
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
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
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
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
|
-
|
|
1872
|
-
|
|
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
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
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
|
-
|
|
1885
|
-
return {
|
|
1886
|
-
|
|
1887
|
-
|
|
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
|
|
1903
|
-
return { path:
|
|
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
|
|
1954
|
-
|
|
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
|
|
2117
|
+
const c = verification = await verifyLesson(lesson);
|
|
1976
2118
|
compileStatus = c.status;
|
|
1977
|
-
compileErrors = c.projects.filter((p) => p.
|
|
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
|
|
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
|
-
|
|
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 =
|
|
2054
|
-
|
|
2055
|
-
const file =
|
|
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
|
-
|
|
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
|
|
2070
|
-
import
|
|
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 =
|
|
2084
|
-
|
|
2085
|
-
const file =
|
|
2086
|
-
|
|
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:
|
|
3646
|
-
content: [{ type: "text", text: getLessonFileTool(courseId, lessonId,
|
|
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
|
-
"
|
|
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
|
|
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 &&
|
|
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),
|