@fullstackcraftllc/codevideo-mcp 1.0.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 (31) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +85 -0
  3. package/bin/darwin-arm64/codevideo-cli +0 -0
  4. package/bin/darwin-x64/codevideo-cli +0 -0
  5. package/bin/linux-x64/codevideo-cli +0 -0
  6. package/bin/win32-x64/codevideo-cli.exe +0 -0
  7. package/dist/index.js +1372 -0
  8. package/dist/src/index.d.ts +1 -0
  9. package/dist/src/prompts/instructions_to_create_a_course.d.ts +1 -0
  10. package/dist/src/prompts/instructions_to_create_lesson_actions.d.ts +1 -0
  11. package/dist/src/prompts/instructions_to_create_lesson_video.d.ts +1 -0
  12. package/dist/src/resources/get_action_names.d.ts +4 -0
  13. package/dist/src/resources/get_example_lesson.d.ts +1 -0
  14. package/dist/src/tools/add_actions_to_lesson.d.ts +2 -0
  15. package/dist/src/tools/add_final_snapshot_to_lesson.d.ts +2 -0
  16. package/dist/src/tools/add_initial_snapshot_to_lesson.d.ts +2 -0
  17. package/dist/src/tools/add_lesson_to_course.d.ts +2 -0
  18. package/dist/src/tools/create_course_with_initial_metadata.d.ts +1 -0
  19. package/dist/src/tools/critique_video_using_gemini.d.ts +2 -0
  20. package/dist/src/tools/get_current_actions.d.ts +3 -0
  21. package/dist/src/tools/get_empty_lesson_snapshot.d.ts +1 -0
  22. package/dist/src/tools/get_final_lesson_snapshot.d.ts +2 -0
  23. package/dist/src/tools/get_initial_lesson_state_from_local_repo.d.ts +1 -0
  24. package/dist/src/tools/is_repeatable_action.d.ts +2 -0
  25. package/dist/src/tools/make_blog_post.d.ts +2 -0
  26. package/dist/src/tools/make_html_web_page.d.ts +2 -0
  27. package/dist/src/tools/make_video.d.ts +8 -0
  28. package/dist/src/tools/set_current_actions.d.ts +2 -0
  29. package/dist/src/tools/validate_actions.d.ts +6 -0
  30. package/dist/src/utils/getBinaryPath.d.ts +1 -0
  31. package/package.json +65 -0
package/dist/index.js ADDED
@@ -0,0 +1,1372 @@
1
+ import dotenv from 'dotenv';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { z } from 'zod';
5
+ import { AllActionStrings, isRepeatableAction, isValidAction } from '@fullstackcraftllc/codevideo-types';
6
+ import { VirtualIDE } from '@fullstackcraftllc/codevideo-virtual-ide';
7
+ import Database from 'better-sqlite3';
8
+ import path, { join } from 'path';
9
+ import os, { platform, arch } from 'os';
10
+ import fs, { existsSync } from 'fs';
11
+ import { buildFileStructureFromRootFolder } from '@fullstackcraftllc/codevideo-adapters';
12
+ import { GoogleGenAI } from '@google/genai';
13
+ import { generateMarkdownStringFromActions, generateHtmlStringFromActions } from '@fullstackcraftllc/codevideo-exporters';
14
+ import { spawn } from 'child_process';
15
+
16
+ var version = "1.0.0";
17
+
18
+ // this prompt describes how to create a course lesson using the CodeVideo framework
19
+ const instructionsToCreateACourse = (additionalContext) => {
20
+ return `You are an expert senior software engineer and educator, and skilled in describing software concepts in a way that is easy for students to understand. You are going to create a course using the CodeVideo framework, a framework that uses simple JSON to generate software courses and lessons in a variety of formats (video, markdown, pdf, and more).
21
+
22
+ There are a few rules to uphold when making a CodeVideo course:
23
+
24
+ 1. The final course snapshot of the previous lesson must be equivalent to the initial course snapshot of the next lesson.
25
+ 2. Beyond the first lesson, each lesson must start with an \`author-speak-before\` action that summarizes the previous lesson with a maximum of 3 sentences.
26
+ 3. Beyond the first lesson, each lesson must have their second action be an \`author-speak-during\` action that summarizes the current lesson with a maximum of 3 sentences.
27
+ 4. Beyond the first lesson, each lesson must end with an \`author-speak-after\` action that summarizes the current lesson with a maximum of 3 sentences.
28
+
29
+ With these rules in mind, please create a course with the following lessons:
30
+
31
+ ${additionalContext}
32
+
33
+ Now, you should first think of a name and description for this course, they should be SEO friendly title and meta description respectively.
34
+
35
+ To create the initial course object, you can call the MCP tool \`codevideo_create_course_with_initial_metadata\` with the following parameters:
36
+ - id: a unique identifier for the course
37
+ - name: the name of the course
38
+ - description: the description of the course
39
+ - primaryLanguage: the primary language of the course
40
+
41
+ You should then plan out the number of lessons and what they will be about. To add a lesson to the course, you can call the MCP tool \`codevideo_add_lesson_to_course\` with the following parameters:
42
+ - id: a unique identifier for the lesson. it should relate to the course id, for example if the course id is \`generics-in-typescript\`, the lesson id should be something like \`generics-in-typescript-lesson-1-introduction\`
43
+ - name: the name of the lesson
44
+ - description: the description of the lesson
45
+
46
+ You then need to create the initial lesson snapshot. For the very first lesson, you can call the tool \`codevideo_get_empty_lesson_snapshot\` without any parameters to get an empty lesson snapshot object. For the rest of the lessons, you can copy the final lesson snapshot of the previous lesson and use that as the initial lesson snapshot.
47
+
48
+ You then need to add actions to the lesson. First read the prompt in \`codevideo_instructions_to_create_lesson_actions\` to get the instructions on how to create lesson actions.
49
+
50
+ When done creating actions, you can add actions to the lesson by calling the MCP tool \`codevideo_add_actions_to_lesson\` with the following parameters:
51
+ - lesson: the ILesson object that you created earlier
52
+ - actions: the array of IAction objects that you created earlier
53
+
54
+ WORKFLOW TIP: For efficiency, after creating actions for each lesson, you can call \`codevideo_set_current_actions\` to store them, then use \`codevideo_get_final_lesson_snapshot\` without the actions parameter (it will use the stored actions automatically).
55
+
56
+ For each lesson, you also need to create the final lesson snapshot. You can get the final snapshot the lesson by calling the MCP tool \`codevideo_get_final_lesson_snapshot\` with the following parameters:
57
+ - actions: the array of IAction objects that you created earlier for the lesson (OR omit this parameter if you've called \`codevideo_set_current_actions\` first)
58
+
59
+ In summary, the steps to create a course are:
60
+
61
+ 1. Create the initial course object using the MCP tool \`codevideo_create_course_with_initial_metadata\`
62
+ 2. Plan out the number of lessons needed and what they will be about
63
+ 3. For each lesson, create a unique identifier, name, and description
64
+ 4. Create the initial empty lesson snapshot for the first lesson using the MCP tool \`codevideo_get_empty_lesson_snapshot\`. In the case of lessons beyond the first, copy the final lesson snapshot of the previous lesson and use that as the initial lesson snapshot.
65
+ 5. Create actions for the lesson using the MCP tool \`codevideo_instructions_to_create_lesson_actions\`
66
+ 6. Add actions to the lesson using the MCP tool \`codevideo_add_actions_to_lesson\`
67
+ 7. Create the final lesson snapshot using the MCP tool \`codevideo_get_final_lesson_snapshot\`
68
+ 8. Add the lesson to the course using the MCP tool \`codevideo_add_lesson_to_course\`
69
+ 9. Repeat steps 4-8 for each lesson
70
+
71
+ When done, return the completed course object as a JSON string.
72
+
73
+ Good luck, and enjoy creating your course!`;
74
+ };
75
+
76
+ // this prompt describes how to make lesson actions in the CodeVideo framework
77
+ const instructionsToCreateLessonActions = (additionalContext) => {
78
+ return `You are an expert senior software engineer and educator, and skilled in describing software concepts in a way that is easy for students to understand. You are going to create lesson actions using the CodeVideo framework, a framework that uses simple JSON to generate tutorials in a variety of formats (video, markdown, pdf, and more). This JSON array is always an array of simple \`IAction\`:
79
+
80
+ \`\`\`typescript
81
+ export interface IAction {
82
+ name: AllActions;
83
+ value: string;
84
+ }
85
+ \`\`\`
86
+
87
+
88
+ Where \`AllActions\` is:
89
+
90
+ \`\`\`typescript
91
+ import { AuthorActions } from "./AuthorActions";
92
+ import { MouseActions } from "./MouseActions";
93
+ import { FileExplorerActions } from "./FileExplorerActions";
94
+ import { TerminalActions } from "./TerminalActions";
95
+ import { EditorActions } from "./EditorActions";
96
+ import { ExternalActions } from "./ExternalActions";
97
+
98
+ // all actions combines the 5 domains of the CodeVideo world
99
+ export type AllActions =
100
+ | AuthorActions
101
+ | FileExplorerActions
102
+ | EditorActions
103
+ | MouseActions
104
+ | TerminalActions
105
+ | AuthorActions
106
+ | ExternalActions;
107
+ \`\`\`
108
+
109
+ Here are all the action types you have to choose from:
110
+
111
+ \`\`\`typescript
112
+ export const AllActionStrings = [
113
+ ${AllActionStrings.map((action) => `"${action}"`).join(",\n ")}
114
+ ];
115
+ \`\`\`
116
+
117
+ When in doubt with these names, feel free to use the MCP tool \`codevideo_get_action_names\` to get the most updated list of all the actions available. You can also use the MCP tool \`codevideo_get_example_lesson\` to get verified, accurate, and correct lessons created by humans.
118
+
119
+ Also VERY IMPORTANT to note, while most 'value' properties are corresponding strings, things like \`editor-enter\` are considered "Repeatable" properties, which MUST have a string number as the 'value' property, which is 99% of the time the string "1", but can be any positive integer, representing the number of times to repeat. An action can be checked for repeatability with the MCP tool \`codevideo_is_repeatable_action\`. To be stressed again: the "value" property of any codevideo action CANNOT be left empty!
120
+
121
+ WORKFLOW TIP: After creating actions, you can store them using \`codevideo_set_current_actions\` and then use other tools like \`codevideo_validate_actions\`, \`codevideo_make_video\`, \`codevideo_make_blog_post\`, etc. without needing to re-specify the actions each time. These tools will automatically retrieve the stored actions if no actions parameter is provided.
122
+
123
+ RECOMMENDED WORKFLOW:
124
+ 1. Create your actions array
125
+ 2. Call \`codevideo_set_current_actions\` once with the actions
126
+ 3. Call \`codevideo_validate_actions\` WITHOUT the actions parameter - it will use the stored actions automatically - if issues are found, fix them in the actions array and call \`codevideo_set_current_actions\` again
127
+ 4. Call any other tools (make_video, make_blog_post, etc.) WITHOUT the actions parameter - they will use the stored actions automatically
128
+ 5. Use \`codevideo_get_current_actions\` at any later time to inspect what's currently stored if needed
129
+
130
+ With all this in mind, you are to create a lesson about the following:
131
+
132
+ ${additionalContext}
133
+
134
+ Be sure to include a corresponding initial \`author-speak-before\` action and a final summary \`author-speak-before\` action.
135
+ `;
136
+ };
137
+
138
+ // this prompt describes how to make lesson actions in the CodeVideo framework
139
+ const instructionsToCreateLessonVideo = (additionalContext) => {
140
+ return `You are an expert senior software engineer and educator, and skilled in describing software concepts in a way that is easy for students to understand. You are going to create a video lesson using the CodeVideo framework, a framework that uses simple JSON to generate tutorials in a variety of formats (video, markdown, pdf, and more). The JSON array used to create video is always an array of simple type \`IAction\`:
141
+
142
+ \`\`\`typescript
143
+ export interface IAction {
144
+ name: AllActions;
145
+ value: string;
146
+ }
147
+ \`\`\`
148
+
149
+
150
+ Where \`AllActions\` is:
151
+
152
+ \`\`\`typescript
153
+ import { AuthorActions } from "./AuthorActions";
154
+ import { MouseActions } from "./MouseActions";
155
+ import { FileExplorerActions } from "./FileExplorerActions";
156
+ import { TerminalActions } from "./TerminalActions";
157
+ import { EditorActions } from "./EditorActions";
158
+ import { ExternalActions } from "./ExternalActions";
159
+
160
+ // all actions combines the 5 domains of the CodeVideo world
161
+ export type AllActions =
162
+ | AuthorActions
163
+ | FileExplorerActions
164
+ | EditorActions
165
+ | MouseActions
166
+ | TerminalActions
167
+ | AuthorActions
168
+ | ExternalActions;
169
+ \`\`\`
170
+
171
+ Here are all the action types you have to choose from:
172
+
173
+ \`\`\`typescript
174
+ export const AllActionStrings = [
175
+ ${AllActionStrings.map((action) => `"${action}"`).join(",\n ")}
176
+ ];
177
+ \`\`\`
178
+
179
+ When in doubt with these names, feel free to use the MCP tool \`codevideo_get_action_names\` to get the most updated list of all the actions available. You can also use the MCP tool \`codevideo_get_example_lesson\` to get verified, accurate, and correct lessons created by humans.
180
+
181
+ Also VERY IMPORTANT to note, while most 'value' properties are corresponding strings, things like \`editor-enter\` are considered "Repeatable" properties, which MUST have a string number as the 'value' property, which is 99% of the time the string "1", but can be any positive integer, representing the number of times to repeat. An action can be checked for repeatability with the MCP tool \`codevideo_is_repeatable_action\`. To be stressed again: the "value" property of any codevideo action CANNOT be left empty!
182
+
183
+ RECOMMENDED WORKFLOW:
184
+ 1. Create your actions array
185
+ 2. Call \`codevideo_set_current_actions\` once with the actions
186
+ 3. Call \`codevideo_validate_actions\` WITHOUT the actions parameter - it will use the stored actions automatically - if issues are found, fix them in the actions array and call \`codevideo_set_current_actions\` again
187
+ 4. Call \`codevideo_make_video\` WITHOUT the actions parameter - it will use the stored actions automatically
188
+
189
+ With all this in mind, you are to create a lesson about the following:
190
+
191
+ ${additionalContext}
192
+
193
+ Be sure to include a corresponding initial \`author-speak-before\` action and a final summary \`author-speak-before\` action.
194
+ `;
195
+ };
196
+
197
+ // retrieves the current actions from the key value sqlite database
198
+ const getCurrentActions = () => {
199
+ try {
200
+ const homeDir = os.homedir();
201
+ const dbPath = path.join(homeDir, '.codevideo', 'codevideo.db');
202
+ // Check if database file exists
203
+ if (!fs.existsSync(dbPath)) {
204
+ return null;
205
+ }
206
+ // Open database
207
+ const db = new Database(dbPath);
208
+ // Check if table exists
209
+ const tableExists = db.prepare(`
210
+ SELECT name FROM sqlite_master
211
+ WHERE type='table' AND name='key_value_store'
212
+ `).get();
213
+ if (!tableExists) {
214
+ db.close();
215
+ return null;
216
+ }
217
+ // Retrieve current_actions
218
+ const stmt = db.prepare(`
219
+ SELECT value FROM key_value_store
220
+ WHERE key = ?
221
+ `);
222
+ const result = stmt.get('current_actions');
223
+ db.close();
224
+ if (!result) {
225
+ return null;
226
+ }
227
+ // Parse JSON back to actions array
228
+ const actions = JSON.parse(result.value);
229
+ return actions;
230
+ }
231
+ catch (error) {
232
+ console.error('Error retrieving actions:', error);
233
+ return null;
234
+ }
235
+ };
236
+ // Helper function that returns actions or throws descriptive error
237
+ const getCurrentActionsOrThrow = () => {
238
+ const actions = getCurrentActions();
239
+ if (!actions) {
240
+ throw new Error('No actions found in storage. Please use codevideo_set_current_actions first to store actions, then try again.');
241
+ }
242
+ return actions;
243
+ };
244
+
245
+ const getFinalLessonSnapshot = (actions) => {
246
+ // Use provided actions or retrieve from storage
247
+ const actionsToUse = actions || getCurrentActionsOrThrow();
248
+ // create a new instance of codevideo virtual ide
249
+ const virtualIDE = new VirtualIDE();
250
+ // apply all actions to the virtual IDE
251
+ virtualIDE.applyActions(actionsToUse);
252
+ // get the final snapshot of the virtual IDE
253
+ const finalSnapshot = virtualIDE.getCourseSnapshot();
254
+ // return the final snapshot as a string
255
+ return JSON.stringify(finalSnapshot, null, 2);
256
+ };
257
+
258
+ const getInitialLessonStateFromLocalCodebase = async (repoLocation) => {
259
+ // build file structure from files and content from the file system adapter
260
+ const fileStructure = await buildFileStructureFromRootFolder(repoLocation);
261
+ // build the project using the file structure
262
+ const project = {
263
+ id: "",
264
+ name: "",
265
+ description: "",
266
+ actions: [],
267
+ initialSnapshot: {
268
+ isUnsavedChangesDialogOpen: false,
269
+ unsavedFileName: '',
270
+ fileExplorerSnapshot: {
271
+ isFileExplorerContextMenuOpen: false,
272
+ isFileContextMenuOpen: false,
273
+ isFolderContextMenuOpen: false,
274
+ isNewFileInputVisible: false,
275
+ isNewFolderInputVisible: false,
276
+ isRenameFileInputVisible: false,
277
+ isRenameFolderInputVisible: false,
278
+ newFileInputValue: '',
279
+ newFolderInputValue: '',
280
+ renameFileInputValue: '',
281
+ renameFolderInputValue: '',
282
+ originalFileBeingRenamed: '',
283
+ originalFolderBeingRenamed: '',
284
+ newFileParentPath: '',
285
+ newFolderParentPath: '',
286
+ fileStructure,
287
+ },
288
+ editorSnapshot: {
289
+ isEditorContextMenuOpen: false,
290
+ editors: []
291
+ },
292
+ terminalSnapshot: {
293
+ terminals: []
294
+ },
295
+ mouseSnapshot: {
296
+ location: 'editor',
297
+ currentHoveredFileName: '',
298
+ currentHoveredFolderName: '',
299
+ currentHoveredEditorTabFileName: '',
300
+ x: 0,
301
+ y: 0,
302
+ timestamp: 0,
303
+ type: 'move',
304
+ buttonStates: {
305
+ left: false,
306
+ right: false,
307
+ middle: false,
308
+ },
309
+ scrollPosition: {
310
+ x: 0,
311
+ y: 0
312
+ },
313
+ },
314
+ authorSnapshot: {
315
+ authors: [
316
+ { currentSpeechCaption: '' }
317
+ ]
318
+ }
319
+ }
320
+ };
321
+ // return the project as a string
322
+ return JSON.stringify(project);
323
+ };
324
+
325
+ const isRepeatable = (action) => {
326
+ return isRepeatableAction(action) ? "true" : "false";
327
+ };
328
+
329
+ const createCourseWithInitialMetadata = (id, name, description, primaryLanguage) => {
330
+ const course = {
331
+ id,
332
+ name,
333
+ description,
334
+ primaryLanguage,
335
+ lessons: []
336
+ };
337
+ return JSON.stringify(course, null, 2);
338
+ };
339
+
340
+ const addLessonToCourse = (course, id, name, description) => {
341
+ const lesson = {
342
+ id,
343
+ name,
344
+ description,
345
+ actions: [],
346
+ initialSnapshot: {
347
+ isUnsavedChangesDialogOpen: false,
348
+ unsavedFileName: '',
349
+ fileExplorerSnapshot: {
350
+ isFileExplorerContextMenuOpen: false,
351
+ isFileContextMenuOpen: false,
352
+ isFolderContextMenuOpen: false,
353
+ isNewFileInputVisible: false,
354
+ isNewFolderInputVisible: false,
355
+ isRenameFileInputVisible: false,
356
+ isRenameFolderInputVisible: false,
357
+ newFileInputValue: '',
358
+ newFolderInputValue: '',
359
+ renameFileInputValue: '',
360
+ renameFolderInputValue: '',
361
+ originalFileBeingRenamed: '',
362
+ originalFolderBeingRenamed: '',
363
+ newFileParentPath: '',
364
+ newFolderParentPath: '',
365
+ fileStructure: {},
366
+ },
367
+ editorSnapshot: {
368
+ isEditorContextMenuOpen: false,
369
+ editors: []
370
+ },
371
+ terminalSnapshot: {
372
+ terminals: []
373
+ },
374
+ mouseSnapshot: {
375
+ location: 'editor',
376
+ currentHoveredFileName: '',
377
+ currentHoveredFolderName: '',
378
+ currentHoveredEditorTabFileName: '',
379
+ x: 0,
380
+ y: 0,
381
+ timestamp: 0,
382
+ type: 'move',
383
+ buttonStates: {
384
+ left: false,
385
+ right: false,
386
+ middle: false,
387
+ },
388
+ scrollPosition: {
389
+ x: 0,
390
+ y: 0
391
+ },
392
+ },
393
+ authorSnapshot: {
394
+ authors: [
395
+ { currentSpeechCaption: '' }
396
+ ]
397
+ }
398
+ }
399
+ };
400
+ // Add the lesson to the course
401
+ course.lessons.push(lesson);
402
+ // Return the updated course object
403
+ return JSON.stringify(course, null, 2);
404
+ };
405
+
406
+ const addActionsToLesson = (lesson, actions) => {
407
+ lesson.actions = actions;
408
+ return JSON.stringify(lesson, null, 2);
409
+ };
410
+
411
+ const getEmptyLessonSnapshot = () => {
412
+ const snapshot = {
413
+ isUnsavedChangesDialogOpen: false,
414
+ unsavedFileName: '',
415
+ fileExplorerSnapshot: {
416
+ isFileExplorerContextMenuOpen: false,
417
+ isFileContextMenuOpen: false,
418
+ isFolderContextMenuOpen: false,
419
+ isNewFileInputVisible: false,
420
+ isNewFolderInputVisible: false,
421
+ isRenameFileInputVisible: false,
422
+ isRenameFolderInputVisible: false,
423
+ newFileInputValue: '',
424
+ newFolderInputValue: '',
425
+ renameFileInputValue: '',
426
+ renameFolderInputValue: '',
427
+ originalFileBeingRenamed: '',
428
+ originalFolderBeingRenamed: '',
429
+ newFileParentPath: '',
430
+ newFolderParentPath: '',
431
+ fileStructure: {},
432
+ },
433
+ editorSnapshot: {
434
+ isEditorContextMenuOpen: false,
435
+ editors: []
436
+ },
437
+ terminalSnapshot: {
438
+ terminals: []
439
+ },
440
+ mouseSnapshot: {
441
+ location: 'editor',
442
+ currentHoveredFileName: '',
443
+ currentHoveredFolderName: '',
444
+ currentHoveredEditorTabFileName: '',
445
+ x: 0,
446
+ y: 0,
447
+ timestamp: 0,
448
+ type: 'move',
449
+ buttonStates: {
450
+ left: false,
451
+ right: false,
452
+ middle: false,
453
+ },
454
+ scrollPosition: {
455
+ x: 0,
456
+ y: 0
457
+ },
458
+ },
459
+ authorSnapshot: {
460
+ authors: [
461
+ { currentSpeechCaption: '' }
462
+ ]
463
+ }
464
+ };
465
+ // return the snapshot as a string
466
+ return JSON.stringify(snapshot, null, 2);
467
+ };
468
+
469
+ const critiqueVideoUsingGemini = async (videoPath, actions) => {
470
+ // Use provided actions or retrieve from storage
471
+ const actionsToUse = actions || getCurrentActionsOrThrow();
472
+ // Check if GEMINI_API_KEY is set in the environment
473
+ const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
474
+ if (!GEMINI_API_KEY) {
475
+ throw new Error('GEMINI_API_KEY environment variable is not set');
476
+ }
477
+ // Initialize the Gemini API client
478
+ const ai = new GoogleGenAI({ apiKey: GEMINI_API_KEY });
479
+ const prompt = `You are an expert senior software engineer and educator, and skilled in describing software concepts in a way that is easy for students to understand. You are going to critique a procedurally generated software video lesson for clarity, accuracy, and correctness. The video and the lesson actions are attached. Your tasks:
480
+
481
+ 1. Watch the video and read the lesson actions.
482
+ 2. Critique the video for clarity, accuracy, and correctness. Of particular note is any audio artifacts or strange wordings. Do not suggest any new material, only critique the existing material.
483
+ 3. Provide a summary of your critique, including any errors or inaccuracies you found, and at what action they occur at.`;
484
+ try {
485
+ // Read the video file as a binary blob for the API
486
+ const videoBuffer = fs.readFileSync(videoPath);
487
+ const videoBase64 = videoBuffer.toString('base64');
488
+ // Convert actions array to JSON string
489
+ const actionsJson = JSON.stringify(actionsToUse, null, 2);
490
+ // Prepare the request with content parts including text and file data
491
+ const response = await ai.models.generateContent({
492
+ model: 'gemini-2.5-pro-preview-05-06',
493
+ contents: [
494
+ {
495
+ role: 'user',
496
+ parts: [
497
+ { text: prompt },
498
+ {
499
+ fileData: {
500
+ mimeType: 'video/mp4',
501
+ fileUri: `data:video/mp4;base64,${videoBase64}`
502
+ }
503
+ },
504
+ {
505
+ text: `Here are the lesson actions in JSON format:\n\n${actionsJson}`
506
+ }
507
+ ]
508
+ }
509
+ ]
510
+ });
511
+ // Get the response text
512
+ const result = response.text;
513
+ return result || "No critique was generated";
514
+ }
515
+ catch (error) {
516
+ console.error('Error during Gemini API call:', error);
517
+ const errorMessage = error instanceof Error ? error.message : String(error);
518
+ throw new Error(`Failed to critique video using Gemini: ${errorMessage}`);
519
+ }
520
+ };
521
+
522
+ // sets the current actions to the key value sqlite database
523
+ const setCurrentActions = (actions) => {
524
+ try {
525
+ // Create database directory if it doesn't exist
526
+ const homeDir = os.homedir();
527
+ const dbDir = path.join(homeDir, '.codevideo');
528
+ if (!fs.existsSync(dbDir)) {
529
+ fs.mkdirSync(dbDir, { recursive: true });
530
+ }
531
+ const dbPath = path.join(dbDir, 'codevideo.db');
532
+ // Open or create database
533
+ const db = new Database(dbPath);
534
+ // Create table if it doesn't exist
535
+ db.exec(`
536
+ CREATE TABLE IF NOT EXISTS key_value_store (
537
+ key TEXT PRIMARY KEY,
538
+ value TEXT NOT NULL,
539
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
540
+ )
541
+ `);
542
+ // Prepare statement to insert or replace the current_actions
543
+ const stmt = db.prepare(`
544
+ INSERT OR REPLACE INTO key_value_store (key, value, updated_at)
545
+ VALUES (?, ?, CURRENT_TIMESTAMP)
546
+ `);
547
+ // Convert actions to JSON string and store
548
+ const actionsJson = JSON.stringify(actions);
549
+ stmt.run('current_actions', actionsJson);
550
+ db.close();
551
+ return `Successfully stored ${actions.length} actions in local database at ${dbPath}`;
552
+ }
553
+ catch (error) {
554
+ return `Error storing actions: ${error.message}`;
555
+ }
556
+ };
557
+
558
+ // calls the markdown generator from the codevideo-exporters package
559
+ const makeBlogPost = (actions) => {
560
+ // Use provided actions or retrieve from storage
561
+ const actionsToUse = actions || getCurrentActionsOrThrow();
562
+ return generateMarkdownStringFromActions(actionsToUse);
563
+ };
564
+
565
+ // calls the HTML generator from the codevideo-exporters package
566
+ const makeHTMLWebPage = async (actions) => {
567
+ // Use provided actions or retrieve from storage
568
+ const actionsToUse = actions || getCurrentActionsOrThrow();
569
+ // Generate the HTML content
570
+ const htmlContent = await generateHtmlStringFromActions(actionsToUse);
571
+ // Create output file on desktop with timestamp
572
+ const homeDir = os.homedir();
573
+ const desktopDir = path.join(homeDir, 'Desktop');
574
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
575
+ const outputFile = path.join(desktopDir, `codevideo-${timestamp}.html`);
576
+ // Write HTML to file
577
+ fs.writeFileSync(outputFile, htmlContent, 'utf8');
578
+ // Open the HTML file in the default browser
579
+ const openChild = spawn('open', [outputFile], {
580
+ detached: true,
581
+ stdio: 'ignore'
582
+ });
583
+ openChild.unref();
584
+ // Return the HTML content
585
+ return htmlContent;
586
+ };
587
+
588
+ function getBinaryPath() {
589
+ const platformMap = {
590
+ 'darwin': 'darwin',
591
+ 'linux': 'linux',
592
+ 'win32': 'win32'
593
+ };
594
+ const archMap = {
595
+ 'x64': 'x64',
596
+ 'arm64': 'arm64'
597
+ };
598
+ const targetPlatform = platformMap[platform()];
599
+ const targetArch = archMap[arch()];
600
+ if (!targetPlatform || !targetArch) {
601
+ throw new Error(`Unsupported platform: ${platform()}-${arch()}`);
602
+ }
603
+ const binaryName = platform() === 'win32' ? 'codevideo-cli.exe' : 'codevideo-cli';
604
+ const binaryPath = join(__dirname, '..', 'bin', `${targetPlatform}-${targetArch}`, binaryName);
605
+ if (!existsSync(binaryPath)) {
606
+ throw new Error(`Binary not found for ${targetPlatform}-${targetArch} at ${binaryPath}`);
607
+ }
608
+ return binaryPath;
609
+ }
610
+
611
+ const makeVideo = async (actions) => {
612
+ try {
613
+ const binaryPath = getBinaryPath(); // Use bundled binary instead of PATH_TO_CODEVIDEO_CLI
614
+ // Use provided actions or retrieve from storage
615
+ const actionsToUse = actions || getCurrentActionsOrThrow();
616
+ // Create JSON string directly
617
+ const actionsJson = JSON.stringify(actionsToUse);
618
+ // Create output file on desktop with timestamp
619
+ const homeDir = os.homedir();
620
+ const desktopDir = path.join(homeDir, 'Desktop');
621
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
622
+ const outputFile = path.join(desktopDir, `codevideo-${timestamp}.mp4`);
623
+ // Fire-and-forget: Start the video generation process in the background
624
+ const child = spawn(binaryPath, ['make-video'], {
625
+ stdio: ['pipe', 'pipe', 'pipe'],
626
+ cwd: process.cwd()
627
+ });
628
+ // Unreference the child process, allowing the parent to exit independently
629
+ child.unref();
630
+ // Return immediately with status message
631
+ return {
632
+ outputPath: outputFile,
633
+ stdout: `Video generation job with ${actionsToUse.length} actions started in the background via the codevideo-cli command. MP4 file path: ${outputFile}. It should open automatically as soon as it is done being rendered.`,
634
+ stderr: ''
635
+ };
636
+ }
637
+ catch (error) {
638
+ console.error('Error starting video generation:', error);
639
+ return {
640
+ outputPath: '',
641
+ stdout: '',
642
+ stderr: `Failed to start video generation: ${error.message}`,
643
+ error
644
+ };
645
+ }
646
+ };
647
+
648
+ /**
649
+ * Validates the actions so the LLM can correct any mistakes it makes
650
+ *
651
+ */
652
+ const validateActions = (actions) => {
653
+ // Use provided actions or retrieve from storage
654
+ const actionsToUse = actions || getCurrentActionsOrThrow();
655
+ // loop over all actions and call the isValidAction function from the types package
656
+ const errorMessages = [];
657
+ for (var i = 0; i < actionsToUse.length; i++) {
658
+ const action = actionsToUse[i];
659
+ // codevideo-types package should cover all action types - normal and repeatable!
660
+ if (!isValidAction(action)) {
661
+ errorMessages.push(`Invalid action at action index ${i}: ${JSON.stringify(action)}. Hint: if this action 'name' is a repeatable action, the 'value' property must be a number. To repeat once, simply use the string "1".`);
662
+ }
663
+ }
664
+ if (errorMessages.length > 0) {
665
+ return JSON.stringify({ isError: true, errorMessages });
666
+ }
667
+ return JSON.stringify({ isError: false, errorMessages: [] });
668
+ };
669
+
670
+ /**
671
+ * Returns all action names exported by the codevideo-types package, joined by commas.
672
+ */
673
+ const getActionNames = () => {
674
+ return AllActionStrings.join(", ");
675
+ };
676
+
677
+ const keywordsToExampleMap = [
678
+ {
679
+ keywords: ["console log", "javascript", "js", "log"],
680
+ actions: [
681
+ {
682
+ name: "author-speak-before",
683
+ value: "Today we're going to learn about console.log in JavaScript. Console.log is a function that allows you to print messages to the console, which is useful for debugging and understanding your code better.",
684
+ },
685
+ {
686
+ name: "author-speak-before",
687
+ value: "Let's first create an index.js file where we will write our code.",
688
+ },
689
+ {
690
+ name: "mouse-move-file-explorer",
691
+ value: "1",
692
+ },
693
+ {
694
+ name: "mouse-right-click",
695
+ value: "1",
696
+ },
697
+ {
698
+ name: "mouse-move-file-explorer-context-menu-new-file",
699
+ value: "1",
700
+ },
701
+ {
702
+ name: "mouse-left-click",
703
+ value: "1",
704
+ }
705
+ ]
706
+ },
707
+ {
708
+ keywords: ["golang", "go", "golang console log", "golang log"],
709
+ actions: [
710
+ {
711
+ "name": "author-speak-before",
712
+ "value": "Let's learn how to use the fmt.Println function in Go!"
713
+ },
714
+ {
715
+ "name": "author-speak-before",
716
+ "value": "Let's first make a main.go file."
717
+ },
718
+ {
719
+ "name": "mouse-move-file-explorer",
720
+ "value": "1"
721
+ },
722
+ {
723
+ "name": "mouse-right-click",
724
+ "value": "1"
725
+ },
726
+ {
727
+ "name": "mouse-move-file-explorer-context-menu-new-file",
728
+ "value": "1"
729
+ },
730
+ {
731
+ "name": "mouse-left-click",
732
+ "value": "1"
733
+ },
734
+ {
735
+ "name": "file-explorer-type-new-file-input",
736
+ "value": "main.go"
737
+ },
738
+ {
739
+ "name": "file-explorer-enter-new-file-input",
740
+ "value": "1"
741
+ },
742
+ {
743
+ "name": "author-speak-before",
744
+ "value": "First, we'll need to declare the main package, and then the main function."
745
+ },
746
+ {
747
+ "name": "editor-type",
748
+ "value": "package main"
749
+ },
750
+ {
751
+ "name": "editor-enter",
752
+ "value": "2"
753
+ },
754
+ {
755
+ "name": "author-speak-before",
756
+ "value": "And we'll need the fmt package to use the Println function."
757
+ },
758
+ {
759
+ "name": "editor-type",
760
+ "value": 'import "fmt"'
761
+ },
762
+ {
763
+ "name": "editor-enter",
764
+ "value": "1"
765
+ },
766
+ {
767
+ "name": "editor-type",
768
+ "value": "func main() {"
769
+ },
770
+ {
771
+ "name": "editor-enter",
772
+ "value": "1"
773
+ },
774
+ {
775
+ "name": "author-speak-before",
776
+ "value": "and let's just print 'Hello world!' to the console."
777
+ },
778
+ {
779
+ "name": "editor-type",
780
+ "value": ' fmt.Println("Hello, world!")'
781
+ },
782
+ {
783
+ "name": "editor-enter",
784
+ "value": "1"
785
+ },
786
+ {
787
+ "name": "editor-type",
788
+ "value": "}"
789
+ },
790
+ {
791
+ "name": "editor-arrow-up",
792
+ "value": "1"
793
+ },
794
+ {
795
+ "name": "editor-command-right",
796
+ "value": "1"
797
+ },
798
+ {
799
+ "name": "author-speak-before",
800
+ "value": "and if I wanted to write the value of some variable to the console, I could do that like so:"
801
+ },
802
+ {
803
+ "name": "editor-backspace",
804
+ "value": "32"
805
+ },
806
+ {
807
+ "name": "editor-type",
808
+ "value": " myVariable := 5"
809
+ },
810
+ {
811
+ "name": "editor-enter",
812
+ "value": "1"
813
+ },
814
+ {
815
+ "name": "editor-type",
816
+ "value": " fmt.Println(myVariable)"
817
+ },
818
+ {
819
+ "name": "author-speak-before",
820
+ "value": "Now, when I run this code, I would expect the value of 'myVariable' to be printed to the console. Something like:"
821
+ },
822
+ {
823
+ "name": "editor-enter",
824
+ "value": "1"
825
+ },
826
+ {
827
+ "name": "editor-type",
828
+ "value": " // 5"
829
+ },
830
+ {
831
+ "name": "author-speak-before",
832
+ "value": "Console logging is simple, yet powerful and very useful!"
833
+ }
834
+ ]
835
+ },
836
+ {
837
+ keywords: ["python", "python console log", "python log"],
838
+ actions: [
839
+ {
840
+ "name": "author-speak-before",
841
+ "value": "Let's learn how to use the print function in Python!"
842
+ },
843
+ {
844
+ "name": "author-speak-before",
845
+ "value": "First, let's make a main.py Python file."
846
+ },
847
+ {
848
+ "name": "mouse-move-file-explorer",
849
+ "value": "1"
850
+ },
851
+ {
852
+ "name": "mouse-right-click",
853
+ "value": "1"
854
+ },
855
+ {
856
+ "name": "mouse-move-file-explorer-context-menu-new-file",
857
+ "value": "1"
858
+ },
859
+ {
860
+ "name": "mouse-left-click",
861
+ "value": "1"
862
+ },
863
+ {
864
+ "name": "file-explorer-type-new-file-input",
865
+ "value": "main.py"
866
+ },
867
+ {
868
+ "name": "file-explorer-enter-new-file-input",
869
+ "value": "1"
870
+ },
871
+ {
872
+ "name": "author-speak-before",
873
+ "value": "and let's print 'Hello world!' to the console."
874
+ },
875
+ {
876
+ "name": "editor-type",
877
+ "value": "print('Hello, world!')"
878
+ },
879
+ {
880
+ "name": "author-speak-before",
881
+ "value": "and if I wanted to write the value of some variable to the console, I could do that like so:"
882
+ },
883
+ {
884
+ "name": "editor-backspace",
885
+ "value": "22"
886
+ },
887
+ {
888
+ "name": "editor-type",
889
+ "value": "my_variable = 5"
890
+ },
891
+ {
892
+ "name": "editor-enter",
893
+ "value": "1"
894
+ },
895
+ {
896
+ "name": "editor-type",
897
+ "value": "print(my_variable)"
898
+ },
899
+ {
900
+ "name": "author-speak-before",
901
+ "value": "Now, when I run this code, I would expect the value of 'my_variable' to be printed to the console. Something like:"
902
+ },
903
+ {
904
+ "name": "editor-enter",
905
+ "value": "1"
906
+ },
907
+ {
908
+ "name": "editor-type",
909
+ "value": "# 5"
910
+ },
911
+ {
912
+ "name": "author-speak-before",
913
+ "value": "Console logging is simple, yet powerful and very useful!"
914
+ },
915
+ {
916
+ "name": "author-speak-before",
917
+ "value": "Well done, lets save our file."
918
+ },
919
+ {
920
+ "name": "editor-save",
921
+ "value": "main.py"
922
+ },
923
+ {
924
+ "name": "author-speak-before",
925
+ "value": "Saved!"
926
+ },
927
+ {
928
+ "name": "author-speak-before",
929
+ "value": "Oh... and I've just realized... this file looks a bit strange... let's add the python shebang to the top of the file."
930
+ },
931
+ {
932
+ "name": "editor-arrow-up",
933
+ "value": "4"
934
+ },
935
+ {
936
+ "name": "editor-command-left",
937
+ "value": "1"
938
+ },
939
+ {
940
+ "name": "editor-enter",
941
+ "value": "1"
942
+ },
943
+ {
944
+ "name": "editor-arrow-up",
945
+ "value": "1"
946
+ },
947
+ {
948
+ "name": "editor-type",
949
+ "value": "#!/usr/bin/env python"
950
+ },
951
+ {
952
+ "name": "editor-enter",
953
+ "value": "1"
954
+ },
955
+ {
956
+ "name": "author-speak-before",
957
+ "value": "Ah...much better!"
958
+ },
959
+ {
960
+ "name": "author-speak-before",
961
+ "value": "Let's save the file again."
962
+ },
963
+ {
964
+ "name": "editor-save",
965
+ "value": "main.py"
966
+ },
967
+ {
968
+ "name": "author-speak-before",
969
+ "value": "Saved!"
970
+ },
971
+ {
972
+ "name": "author-speak-before",
973
+ "value": "Now, let's open a terminal and run this file..."
974
+ },
975
+ {
976
+ "name": "terminal-open",
977
+ "value": "1"
978
+ },
979
+ {
980
+ "name": "terminal-type",
981
+ "value": "python main.py"
982
+ },
983
+ {
984
+ "name": "terminal-set-output",
985
+ "value": "5"
986
+ },
987
+ {
988
+ "name": "terminal-enter",
989
+ "value": "1"
990
+ },
991
+ {
992
+ "name": "author-speak-before",
993
+ "value": "Perfect, our code is working as expected! I hope you enjoyed this lesson!"
994
+ }
995
+ ]
996
+ },
997
+ ];
998
+ // returns a JSON representation of an example lesson based on keywords
999
+ // the LLM can use this to learn real example lessons created by humans
1000
+ const getExampleLesson = (keyword) => {
1001
+ const exampleJSONs = [];
1002
+ if (Array.isArray(keyword)) {
1003
+ // if the keyword is an array, loop over all keywords and get the example lesson for each one
1004
+ for (var i = 0; i < keyword.length; i++) {
1005
+ const exampleJSON = getExampleLesson(keyword[i]);
1006
+ exampleJSONs.push(exampleJSON);
1007
+ }
1008
+ return exampleJSONs.join(", ");
1009
+ }
1010
+ else {
1011
+ // if the keyword is a string, get the example lesson for that keyword
1012
+ const exampleJSON = getSingleExampleLesson(keyword);
1013
+ if (exampleJSON) {
1014
+ return exampleJSON;
1015
+ }
1016
+ }
1017
+ // if no keywords match, return an empty string
1018
+ return "";
1019
+ };
1020
+ const getSingleExampleLesson = (keyword) => {
1021
+ // loop over all keywords and find the first one that matches
1022
+ for (var i = 0; i < keywordsToExampleMap.length; i++) {
1023
+ const keywordMap = keywordsToExampleMap[i];
1024
+ if (keywordMap.keywords.includes(keyword)) {
1025
+ return JSON.stringify(keywordMap.actions);
1026
+ }
1027
+ }
1028
+ // if no keywords match, return an empty string
1029
+ return "";
1030
+ };
1031
+
1032
+ // Load environment variables from .env file
1033
+ dotenv.config();
1034
+ const ActionSchema = z.object({
1035
+ name: z.string(),
1036
+ value: z.string()
1037
+ });
1038
+ const CourseSnapshotSchema = z.object({});
1039
+ const LessonSchema = z.object({
1040
+ id: z.string(),
1041
+ name: z.string(),
1042
+ description: z.string(),
1043
+ actions: z.array(ActionSchema),
1044
+ initialSnapshot: CourseSnapshotSchema,
1045
+ finalSnapshot: CourseSnapshotSchema
1046
+ });
1047
+ const CourseSchema = z.object({
1048
+ id: z.string(),
1049
+ name: z.string(),
1050
+ description: z.string(),
1051
+ primaryLanguage: z.string(),
1052
+ lessons: z.array(LessonSchema)
1053
+ });
1054
+ const server = new McpServer({
1055
+ name: "codevideo-mcp",
1056
+ version: version,
1057
+ env: {
1058
+ // preserve existing environment variables
1059
+ ...Object.fromEntries(Object.entries(process.env)
1060
+ .filter(([, value]) => value !== undefined)
1061
+ .map(([key, value]) => [key, value])),
1062
+ MCP_TIMEOUT: String(1200000), // 20 min - needed for video creation waiting
1063
+ }
1064
+ });
1065
+ // Commenting out prompts and resources that aren't showing up
1066
+ // - does Claude just use them internally?
1067
+ /*
1068
+ // add prompts
1069
+ server.prompt(
1070
+ "codevideo_create_lesson",
1071
+ { additionalContext: z.string() },
1072
+ ({ additionalContext }) => ({
1073
+ messages: [{
1074
+ role: "user",
1075
+ content: {
1076
+ type: "text",
1077
+ text: createLesson(additionalContext)
1078
+ }
1079
+ }]
1080
+ })
1081
+ );
1082
+
1083
+ // add resources
1084
+ server.resource(
1085
+ "codevideo_get_action_names",
1086
+ "codevideo_get_action_names://codevideo_get_action_names",
1087
+ (uri) => ({
1088
+ contents: [{
1089
+ uri: uri.href,
1090
+ text: getActionNames()
1091
+ }]
1092
+ })
1093
+ );
1094
+
1095
+ server.resource(
1096
+ "codevideo_get_example_lesson",
1097
+ new ResourceTemplate("codevideo_get_example_lesson://{keyword}", { list: undefined }),
1098
+ async (uri, { keyword }) => ({
1099
+ contents: [{
1100
+ uri: uri.href,
1101
+ text: getExampleLesson(keyword)
1102
+ }]
1103
+ })
1104
+ );
1105
+ */
1106
+ server.tool("codevideo_instructions_to_create_a_course", {
1107
+ additionalContext: z.string()
1108
+ }, async ({ additionalContext }) => {
1109
+ return {
1110
+ content: [{
1111
+ type: "text",
1112
+ text: instructionsToCreateACourse(additionalContext)
1113
+ }]
1114
+ };
1115
+ });
1116
+ server.tool("codevideo_instructions_to_create_lesson_actions", {
1117
+ additionalContext: z.string()
1118
+ }, async ({ additionalContext }) => {
1119
+ return {
1120
+ content: [{
1121
+ type: "text",
1122
+ text: instructionsToCreateLessonActions(additionalContext)
1123
+ }]
1124
+ };
1125
+ });
1126
+ server.tool("codevideo_instructions_to_create_lesson_video", {
1127
+ additionalContext: z.string()
1128
+ }, async ({ additionalContext }) => {
1129
+ return {
1130
+ content: [{
1131
+ type: "text",
1132
+ text: instructionsToCreateLessonVideo(additionalContext)
1133
+ }]
1134
+ };
1135
+ });
1136
+ server.tool("codevideo_get_empty_lesson_snapshot", {}, async () => {
1137
+ return {
1138
+ content: [{
1139
+ type: "text",
1140
+ text: getEmptyLessonSnapshot()
1141
+ }]
1142
+ };
1143
+ });
1144
+ server.tool("codevideo_create_course_with_initial_metadata", {
1145
+ id: z.string(),
1146
+ name: z.string(),
1147
+ description: z.string(),
1148
+ primaryLanguage: z.string()
1149
+ }, async ({ id, name, description, primaryLanguage }) => {
1150
+ return {
1151
+ content: [{
1152
+ type: "text",
1153
+ text: createCourseWithInitialMetadata(id, name, description, primaryLanguage)
1154
+ }]
1155
+ };
1156
+ });
1157
+ server.tool("codevideo_add_actions_to_lesson", {
1158
+ lesson: LessonSchema,
1159
+ actions: z.array(ActionSchema)
1160
+ }, async ({ lesson, actions }) => {
1161
+ // Cast the lesson to ILesson
1162
+ const typedLesson = lesson;
1163
+ // Cast the actions to IAction
1164
+ const typedActions = actions;
1165
+ return {
1166
+ content: [{
1167
+ type: "text",
1168
+ text: addActionsToLesson(typedLesson, typedActions)
1169
+ }]
1170
+ };
1171
+ });
1172
+ server.tool("codevideo_get_final_lesson_snapshot", "Get the final state snapshot after applying CodeVideo actions. Pass actions array OR use previously stored actions from codevideo_set_current_actions (recommended workflow).", {
1173
+ actions: z.array(ActionSchema).optional().describe("Array of CodeVideo actions to apply. Optional if actions were previously stored with codevideo_set_current_actions")
1174
+ }, async ({ actions }) => {
1175
+ // Cast the actions to IAction if provided
1176
+ const typedActions = actions;
1177
+ return {
1178
+ content: [{
1179
+ type: "text",
1180
+ text: getFinalLessonSnapshot(typedActions)
1181
+ }]
1182
+ };
1183
+ });
1184
+ server.tool("codevideo_add_lesson_to_course", {
1185
+ course: CourseSchema,
1186
+ id: z.string(),
1187
+ name: z.string(),
1188
+ description: z.string()
1189
+ }, async ({ course, id, name, description }) => {
1190
+ // Cast the course to ICourse
1191
+ const typedCourse = course;
1192
+ return {
1193
+ content: [{
1194
+ type: "text",
1195
+ text: addLessonToCourse(typedCourse, id, name, description)
1196
+ }]
1197
+ };
1198
+ });
1199
+ server.tool("codevideo_get_action_names", {}, async () => {
1200
+ return {
1201
+ content: [{
1202
+ type: "text",
1203
+ text: getActionNames()
1204
+ }]
1205
+ };
1206
+ });
1207
+ server.tool("codevideo_get_example_lesson", {
1208
+ keyword: z.string()
1209
+ }, async ({ keyword }) => {
1210
+ return {
1211
+ content: [{
1212
+ type: "text",
1213
+ text: getExampleLesson(keyword)
1214
+ }]
1215
+ };
1216
+ });
1217
+ server.tool("codevideo_get_initial_lesson_state_from_local_repo", {
1218
+ repoLocation: z.string()
1219
+ }, async ({ repoLocation }) => {
1220
+ return {
1221
+ content: [{
1222
+ type: "text",
1223
+ text: await getInitialLessonStateFromLocalCodebase(repoLocation)
1224
+ }]
1225
+ };
1226
+ });
1227
+ server.tool("codevideo_make_blog_post", "Generate a markdown blog post from CodeVideo actions. Pass actions array OR use previously stored actions from codevideo_set_current_actions (recommended workflow).", {
1228
+ actions: z.array(ActionSchema).optional().describe("Array of CodeVideo actions. Optional if actions were previously stored with codevideo_set_current_actions")
1229
+ }, async ({ actions }) => {
1230
+ // Cast the actions to IAction if provided
1231
+ const typedActions = actions;
1232
+ return {
1233
+ content: [{
1234
+ type: "text",
1235
+ text: makeBlogPost(typedActions)
1236
+ }]
1237
+ };
1238
+ });
1239
+ server.tool("codevideo_make_html_web_page", "Generate an HTML web page from CodeVideo actions. Pass actions array OR use previously stored actions from codevideo_set_current_actions (recommended workflow).", {
1240
+ actions: z.array(ActionSchema).optional().describe("Array of CodeVideo actions. Optional if actions were previously stored with codevideo_set_current_actions")
1241
+ }, async ({ actions }) => {
1242
+ // Cast the actions to IAction if provided
1243
+ const typedActions = actions;
1244
+ return {
1245
+ content: [{
1246
+ type: "text",
1247
+ text: await makeHTMLWebPage(typedActions)
1248
+ }]
1249
+ };
1250
+ });
1251
+ server.tool("codevideo_make_video", "Generate a video from CodeVideo actions. Pass actions array OR use previously stored actions from codevideo_set_current_actions (recommended workflow).", {
1252
+ actions: z.array(ActionSchema).optional().describe("Array of CodeVideo actions. Optional if actions were previously stored with codevideo_set_current_actions")
1253
+ }, async ({ actions }) => {
1254
+ // Cast the actions to IAction if provided
1255
+ const typedActions = actions;
1256
+ try {
1257
+ const result = await makeVideo(typedActions);
1258
+ return {
1259
+ content: [{
1260
+ type: "text",
1261
+ text: JSON.stringify({
1262
+ outputPath: result.outputPath,
1263
+ stdout: result.stdout,
1264
+ stderr: result.stderr
1265
+ }, null, 2)
1266
+ }]
1267
+ };
1268
+ }
1269
+ catch (error) {
1270
+ console.error('Error making video:', error);
1271
+ // If the error has the VideoResult structure
1272
+ if (error && typeof error === 'object' && 'stdout' in error && 'stderr' in error) {
1273
+ return {
1274
+ content: [{
1275
+ type: "text",
1276
+ text: `Error creating video: ${error.error?.message || String(error.error)}\nstdout: ${error.stdout}\nstderr: ${error.stderr}`
1277
+ }]
1278
+ };
1279
+ }
1280
+ // Fall back to simple error message
1281
+ return {
1282
+ content: [{
1283
+ type: "text",
1284
+ text: `Error creating video: ${error?.message || String(error)}`
1285
+ }]
1286
+ };
1287
+ }
1288
+ });
1289
+ server.tool("codevideo_validate_actions", {
1290
+ actions: z.array(ActionSchema).optional()
1291
+ }, async ({ actions }) => {
1292
+ // Cast the actions to IAction if provided
1293
+ const typedActions = actions;
1294
+ return {
1295
+ content: [{
1296
+ type: "text",
1297
+ text: validateActions(typedActions)
1298
+ }]
1299
+ };
1300
+ });
1301
+ server.tool("codevideo_is_repeatable_action", {
1302
+ action: ActionSchema
1303
+ }, async ({ action }) => {
1304
+ // Cast the action to IAction
1305
+ const typedAction = action;
1306
+ return {
1307
+ content: [{
1308
+ type: "text",
1309
+ text: isRepeatable(typedAction)
1310
+ }]
1311
+ };
1312
+ });
1313
+ server.tool("codevideo_critique_video_using_gemini", "Critique a generated video using Google Gemini AI. Pass actions array OR use previously stored actions from codevideo_set_current_actions (recommended workflow).", {
1314
+ videoPath: z.string().describe("Path to the video file to critique"),
1315
+ actions: z.array(ActionSchema).optional().describe("Array of CodeVideo actions used to generate the video. Optional if actions were previously stored with codevideo_set_current_actions")
1316
+ }, async ({ videoPath, actions }) => {
1317
+ // Cast the actions to IAction if provided
1318
+ const typedActions = actions;
1319
+ try {
1320
+ const critique = await critiqueVideoUsingGemini(videoPath, typedActions);
1321
+ return {
1322
+ content: [{
1323
+ type: "text",
1324
+ text: critique
1325
+ }]
1326
+ };
1327
+ }
1328
+ catch (error) {
1329
+ const errorMessage = error instanceof Error ? error.message : String(error);
1330
+ console.error("Error critiquing video:", errorMessage);
1331
+ return {
1332
+ content: [{
1333
+ type: "text",
1334
+ text: `Error critiquing video: ${errorMessage}`
1335
+ }]
1336
+ };
1337
+ }
1338
+ });
1339
+ server.tool("codevideo_set_current_actions", "Store actions in local SQLite database for reuse across multiple tool calls. Once set, other tools can be called without re-specifying actions.", {
1340
+ actions: z.array(ActionSchema).describe("Array of CodeVideo actions to store for later use")
1341
+ }, async ({ actions }) => {
1342
+ // Cast the actions to IAction
1343
+ const typedActions = actions;
1344
+ return {
1345
+ content: [{
1346
+ type: "text",
1347
+ text: setCurrentActions(typedActions)
1348
+ }]
1349
+ };
1350
+ });
1351
+ server.tool("codevideo_get_current_actions", "Retrieve the currently stored actions from local database. Use this to inspect what actions were previously set with codevideo_set_current_actions.", {}, async () => {
1352
+ const actions = getCurrentActions();
1353
+ if (!actions) {
1354
+ return {
1355
+ content: [{
1356
+ type: "text",
1357
+ text: "No actions found in storage. Use codevideo_set_current_actions first to store actions."
1358
+ }]
1359
+ };
1360
+ }
1361
+ return {
1362
+ content: [{
1363
+ type: "text",
1364
+ text: JSON.stringify(actions, null, 2)
1365
+ }]
1366
+ };
1367
+ });
1368
+ // start the server
1369
+ const transport = new StdioServerTransport();
1370
+ (async () => {
1371
+ await server.connect(transport);
1372
+ })();