@fullstackcraftllc/codevideo-mcp 1.0.0 → 1.0.2

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/dist/index.js CHANGED
@@ -3,17 +3,17 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
4
  import { z } from 'zod';
5
5
  import { AllActionStrings, isRepeatableAction, isValidAction } from '@fullstackcraftllc/codevideo-types';
6
- import { VirtualIDE } from '@fullstackcraftllc/codevideo-virtual-ide';
7
6
  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';
7
+ import path from 'path';
8
+ import os from 'os';
9
+ import fs from 'fs';
10
+ import { VirtualIDE } from '@fullstackcraftllc/codevideo-virtual-ide';
11
+ import { buildFileStructureFromRootFolder, buildFileStructureFromGitHub } from '@fullstackcraftllc/codevideo-adapters';
12
12
  import { GoogleGenAI } from '@google/genai';
13
13
  import { generateMarkdownStringFromActions, generateHtmlStringFromActions } from '@fullstackcraftllc/codevideo-exporters';
14
14
  import { spawn } from 'child_process';
15
15
 
16
- var version = "1.0.0";
16
+ var version = "1.0.2";
17
17
 
18
18
  // this prompt describes how to create a course lesson using the CodeVideo framework
19
19
  const instructionsToCreateACourse = (additionalContext) => {
@@ -61,7 +61,7 @@ const instructionsToCreateACourse = (additionalContext) => {
61
61
  1. Create the initial course object using the MCP tool \`codevideo_create_course_with_initial_metadata\`
62
62
  2. Plan out the number of lessons needed and what they will be about
63
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.
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 any lesson beyond the first lesson, generate the final lesson snapshot and copy it as the initial lesson snapshot.
65
65
  5. Create actions for the lesson using the MCP tool \`codevideo_instructions_to_create_lesson_actions\`
66
66
  6. Add actions to the lesson using the MCP tool \`codevideo_add_actions_to_lesson\`
67
67
  7. Create the final lesson snapshot using the MCP tool \`codevideo_get_final_lesson_snapshot\`
@@ -114,18 +114,47 @@ const instructionsToCreateLessonActions = (additionalContext) => {
114
114
  ];
115
115
  \`\`\`
116
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.
117
+ 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_actions_array_by_keyword\` \`codevideo_get_example_lessons_by_keyword\` to get verified, accurate, and correct lessons created by humans.
118
118
 
119
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
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.
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_from_actions\`, \`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
+ LESSON MANAGEMENT: You can also store complete lesson objects using \`codevideo_set_current_lesson\` and retrieve them with \`codevideo_get_current_lesson\`. This is useful for the FULL LESSON OBJECT approach where you need to manage complete lessons with snapshots.
124
+
125
+ ACTIONS vs FULL LESSON OBJECTS:
126
+ You have two approaches for creating lessons:
127
+
128
+ 1. **ACTIONS-ONLY APPROACH**: Use when creating simple tutorials that start from a clean slate or when you only need to demonstrate specific coding steps. Use \`codevideo_make_video_from_actions\` for this approach.
129
+
130
+ 2. **FULL LESSON OBJECT APPROACH**: Use when you need to:
131
+ - Start from an existing codebase (use \`codevideo_get_initial_lesson_state_from_local_repo\` or \`codevideo_get_initial_lesson_state_from_github_repo\`)
132
+ - Continue from a previous lesson's ending state (use \`codevideo_get_final_lesson_snapshot\` from a previous lesson)
133
+ - Have complete control over the initial and final snapshots
134
+ - Use \`codevideo_make_video_from_lesson\` for this approach
122
135
 
123
136
  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
137
+ 1. **DECIDE YOUR APPROACH**:
138
+ - For simple tutorials starting from scratch: Use ACTIONS-ONLY approach
139
+ - For tutorials starting from existing code or continuing from previous lessons: Use FULL LESSON OBJECT approach
140
+
141
+ 2. Call the \`codevideo_get_example_lesson\` tool to examine example lessons to get a feel for the codevideo actions and composition.
142
+ 3. Use the \`codevideo_get_action_names\` tool to get the complete and most up-to-date list of actions.
143
+
144
+ 4. **IF USING FULL LESSON OBJECT APPROACH**:
145
+ 4a. If starting from an existing codebase, use \`codevideo_get_initial_lesson_state_from_local_repo\` or \`codevideo_get_initial_lesson_state_from_github_repo\`
146
+ 4b. If continuing from a previous lesson, use \`codevideo_get_final_lesson_snapshot\` from the previous lesson's actions
147
+ 4c. Create your actions array and build the complete lesson object with initial/final snapshots
148
+ 4d. Optionally store the lesson using \`codevideo_set_current_lesson\` for later retrieval
149
+ 4e. Use \`codevideo_make_video_from_lesson\` with the complete lesson object
150
+
151
+ 5. **IF USING ACTIONS-ONLY APPROACH**:
152
+ 5a. Create the actions array
153
+ 5b. Call \`codevideo_set_current_actions\` once with the actions
154
+ 5c. Call \`codevideo_validate_actions\` WITHOUT the actions parameter - it will use the stored actions automatically
155
+ 5d. Call \`codevideo_make_video_from_actions\` WITHOUT the actions parameter - it will use the stored actions automatically
156
+
157
+ 6. Use \`codevideo_get_current_actions\` or \`codevideo_get_current_lesson\` at any later time to inspect what's currently stored if needed
129
158
 
130
159
  With all this in mind, you are to create a lesson about the following:
131
160
 
@@ -180,11 +209,36 @@ const instructionsToCreateLessonVideo = (additionalContext) => {
180
209
 
181
210
  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
211
 
212
+ LESSON MANAGEMENT: You can store complete lesson objects using \`codevideo_set_current_lesson\` and retrieve them with \`codevideo_get_current_lesson\`. This is particularly useful for the FULL LESSON OBJECT approach where you need to manage complete lessons with snapshots.
213
+
214
+ ACTIONS vs FULL LESSON OBJECTS - CHOOSE YOUR APPROACH:
215
+ You have two approaches for creating video lessons:
216
+
217
+ 1. **ACTIONS-ONLY APPROACH**: Use when creating simple tutorials that start from a clean slate or when you only need to demonstrate specific coding steps. Use \`codevideo_make_video_from_actions\` for this approach.
218
+
219
+ 2. **FULL LESSON OBJECT APPROACH**: Use when you need to:
220
+ - Start from an existing codebase (use \`codevideo_get_initial_lesson_state_from_local_repo\` or \`codevideo_get_initial_lesson_state_from_github_repo\`)
221
+ - Continue from a previous lesson's ending state (use \`codevideo_get_final_lesson_snapshot\` from a previous lesson)
222
+ - Have complete control over the initial and final snapshots
223
+ - Use \`codevideo_make_video_from_lesson\` for this approach
224
+
183
225
  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
226
+ 1. **DECIDE YOUR APPROACH**:
227
+ - For simple tutorials starting from scratch: Use ACTIONS-ONLY approach
228
+ - For tutorials starting from existing code or continuing from previous lessons: Use FULL LESSON OBJECT approach
229
+
230
+ 2. **IF USING ACTIONS-ONLY APPROACH**:
231
+ 2a. Create your actions array
232
+ 2b. Call \`codevideo_set_current_actions\` once with the actions
233
+ 2c. 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
234
+ 2d. Call \`codevideo_make_video_from_actions\` WITHOUT the actions parameter - it will use the stored actions automatically
235
+
236
+ 3. **IF USING FULL LESSON OBJECT APPROACH**:
237
+ 3a. If starting from an existing codebase, use \`codevideo_get_initial_lesson_state_from_local_repo\` or \`codevideo_get_initial_lesson_state_from_github_repo\`
238
+ 3b. If continuing from a previous lesson, use \`codevideo_get_final_lesson_snapshot\` from the previous lesson's actions
239
+ 3c. Create your actions array and build the complete lesson object with initial/final snapshots
240
+ 3d. Optionally store the lesson using \`codevideo_set_current_lesson\` for later retrieval
241
+ 3e. Use \`codevideo_make_video_from_lesson\` with the complete lesson object
188
242
 
189
243
  With all this in mind, you are to create a lesson about the following:
190
244
 
@@ -194,6 +248,10 @@ const instructionsToCreateLessonVideo = (additionalContext) => {
194
248
  `;
195
249
  };
196
250
 
251
+ // Database key constants for the CodeVideo MCP key-value store
252
+ const CURRENT_ACTIONS_KEY = 'current_actions';
253
+ const CURRENT_LESSON_KEY = 'current_lesson';
254
+
197
255
  // retrieves the current actions from the key value sqlite database
198
256
  const getCurrentActions = () => {
199
257
  try {
@@ -219,7 +277,7 @@ const getCurrentActions = () => {
219
277
  SELECT value FROM key_value_store
220
278
  WHERE key = ?
221
279
  `);
222
- const result = stmt.get('current_actions');
280
+ const result = stmt.get(CURRENT_ACTIONS_KEY);
223
281
  db.close();
224
282
  if (!result) {
225
283
  return null;
@@ -242,6 +300,174 @@ const getCurrentActionsOrThrow = () => {
242
300
  return actions;
243
301
  };
244
302
 
303
+ const instructionsToTranslateActions = (actionsToTranslation, targetLanguage) => {
304
+ let actionsToUse;
305
+ if (actionsToTranslation && actionsToTranslation.length > 0) {
306
+ // Use provided actions
307
+ actionsToUse = actionsToTranslation;
308
+ }
309
+ else {
310
+ // Try to get from storage
311
+ const storedActions = getCurrentActions();
312
+ if (!storedActions || storedActions.length === 0) {
313
+ return "No actions provided and no actions found in storage. Either pass actionsToTranslation parameter or use codevideo_set_current_actions first to store actions.";
314
+ }
315
+ actionsToUse = storedActions;
316
+ }
317
+ return `## 📝 Translation Instructions for CodeVideo Action Scripts
318
+
319
+ ### 📌 Objective
320
+
321
+ You are given a JSON array representing an instructional CodeVideo script. Your task is to translate all relevant text **within the \`"value"\` fields** into the specified target language **while preserving CodeVideo execution logic**.
322
+
323
+ **WORKFLOW TIP**: Use \`codevideo_set_current_actions\` to store actions first, then call this tool without the actionsToTranslation parameter. This allows you to work with the same actions across multiple translation operations or other CodeVideo tools like \`codevideo_validate_actions\`, \`codevideo_make_video_from_actions\`, etc.
324
+
325
+ **VALIDATION TIP**: After translation, you can validate the translated actions using \`codevideo_validate_actions\` to ensure the CodeVideo structure remains intact.
326
+
327
+ ---
328
+
329
+ ### ✅ What to Translate
330
+ Translate the following, only if found within the \`"value"\` fields:
331
+
332
+ 1. **All narration steps:**
333
+ - \`"name": "author-speak-before"\` or \`"author-speak-after"\`
334
+ → Fully translate narration into fluent, natural speech in the target language.
335
+
336
+ 2. **Code comments:**
337
+ - Lines starting with \`#\` or \`//\` or between \`/* ... */\`
338
+ → Translate the comment portion only, preserving syntax markers.
339
+
340
+ 3. **String literals inside code:**
341
+ - e.g., \`print("Hello, World!")\`
342
+ → Translate the string inside the quotes, preserving quote marks and code structure.
343
+
344
+ 4. **Terminal outputs:**
345
+ - \`"name": "terminal-set-output"\`
346
+ → Translate printed outputs line by line.
347
+
348
+ ---
349
+
350
+ ### 🚫 What NOT to Translate
351
+ Do **not** translate the following:
352
+
353
+ - \`"name"\` field values (e.g., \`"author-speak-before"\`, \`"editor-type"\`, etc.)
354
+ → These are CodeVideo internal commands.
355
+
356
+ - File names, identifiers, variables, or function names (e.g., \`main.py\`, \`range\`, \`print\`, \`for i in range(...)\`)
357
+ → Keep code functional and consistent.
358
+
359
+ - Keywords or syntax in programming code
360
+ → Only translate *comments* and *strings*.
361
+
362
+ ---
363
+
364
+ ### 💡 Examples
365
+
366
+ #### Code Comment
367
+
368
+ \`\`\`json
369
+ { "name": "editor-type", "value": "# Print 'Hello, World!' five times" }
370
+ \`\`\`
371
+
372
+ ➡ becomes:
373
+
374
+ \`\`\`json
375
+ { "name": "editor-type", "value": "# Imprime '¡Hola, Mundo!' cinco veces" }
376
+ \`\`\`
377
+
378
+ #### Narration
379
+ \`\`\`json
380
+ { "name": "author-speak-before", "value": "Let's open a terminal and run the script." }
381
+ \`\`\`
382
+ ➡ becomes:
383
+ \`\`\`json
384
+ { "name": "author-speak-before", "value": "Vamos a abrir una terminal y ejecutar el script." }
385
+ \`\`\`
386
+
387
+ #### Terminal Output
388
+ \`\`\`json
389
+ { "name": "terminal-set-output", "value": "Hello, World!\nHello, World!" }
390
+ \`\`\`
391
+ ➡ becomes:
392
+ \`\`\`json
393
+ { "name": "terminal-set-output", "value": "¡Hola, Mundo!\n¡Hola, Mundo!" }
394
+ \`\`\`
395
+
396
+ ---
397
+
398
+ ### 🌍 Target Language
399
+
400
+ The target language will be specified separately. Use fluent, natural phrasing appropriate to that language's tone and style of instruction.
401
+
402
+ ---
403
+
404
+ ### 🧠 Tips
405
+
406
+ - Match tone to the original narration (enthusiastic, beginner-friendly).
407
+ - Avoid overly literal translations — clarity is more important.
408
+ - Maintain line breaks, indentation, and special characters exactly as they appear in code or outputs.
409
+
410
+ ---
411
+
412
+ ### ✅ Output Format
413
+
414
+ Return the translated JSON with only the \`value\` fields updated. The JSON structure must remain valid and unchanged otherwise.
415
+
416
+ \`\`\`json
417
+ { "name": "editor-type", "value": "# Print 'Hello, World!' five times" }
418
+ \`\`\`
419
+ ➡ becomes:
420
+ \`\`\`json
421
+ { "name": "editor-type", "value": "# Imprime '¡Hola, Mundo!' cinco veces" }
422
+ \`\`\`
423
+
424
+ #### Narration
425
+ \`\`\`json
426
+ { "name": "author-speak-before", "value": "Let's open a terminal and run the script." }
427
+ \`\`\`
428
+ ➡ becomes:
429
+ \`\`\`json
430
+ { "name": "author-speak-before", "value": "Vamos a abrir una terminal y ejecutar el script." }
431
+ \`\`\`
432
+
433
+ #### Terminal Output
434
+ \`\`\`json
435
+ { "name": "terminal-set-output", "value": "Hello, World!\nHello, World!" }
436
+ \`\`\`
437
+ ➡ becomes:
438
+ \`\`\`json
439
+ { "name": "terminal-set-output", "value": "¡Hola, Mundo!\n¡Hola, Mundo!" }
440
+ \`\`\`
441
+
442
+ ---
443
+
444
+ ### 🌍 Target Language
445
+
446
+ The target language will be specified below. Use fluent, natural phrasing appropriate to that language's tone and style of instruction.
447
+
448
+ ---
449
+
450
+ ### 🧠 Tips
451
+
452
+ - Match tone to the original narration (enthusiastic, beginner-friendly).
453
+ - Avoid overly literal translations — clarity is more important.
454
+ - Maintain line breaks, indentation, and special characters exactly as they appear in code or outputs.
455
+
456
+ ---
457
+
458
+ ### ✅ Output Format
459
+ Return the translated JSON with only the \`"value"\` fields updated. The JSON structure must remain valid and unchanged otherwise.
460
+
461
+
462
+ Considering all these instructions, please translate the following actions into ${targetLanguage}:
463
+
464
+ \`\`\`json
465
+ ${JSON.stringify(actionsToUse, null, 4)}
466
+ \`\`\`
467
+
468
+ `;
469
+ };
470
+
245
471
  const getFinalLessonSnapshot = (actions) => {
246
472
  // Use provided actions or retrieve from storage
247
473
  const actionsToUse = actions || getCurrentActionsOrThrow();
@@ -257,7 +483,74 @@ const getFinalLessonSnapshot = (actions) => {
257
483
 
258
484
  const getInitialLessonStateFromLocalCodebase = async (repoLocation) => {
259
485
  // build file structure from files and content from the file system adapter
260
- const fileStructure = await buildFileStructureFromRootFolder(repoLocation);
486
+ const { fileStructure } = await buildFileStructureFromRootFolder(repoLocation);
487
+ // build the project using the file structure
488
+ const project = {
489
+ id: "",
490
+ name: "",
491
+ description: "",
492
+ actions: [],
493
+ initialSnapshot: {
494
+ isUnsavedChangesDialogOpen: false,
495
+ unsavedFileName: '',
496
+ fileExplorerSnapshot: {
497
+ isFileExplorerContextMenuOpen: false,
498
+ isFileContextMenuOpen: false,
499
+ isFolderContextMenuOpen: false,
500
+ isNewFileInputVisible: false,
501
+ isNewFolderInputVisible: false,
502
+ isRenameFileInputVisible: false,
503
+ isRenameFolderInputVisible: false,
504
+ newFileInputValue: '',
505
+ newFolderInputValue: '',
506
+ renameFileInputValue: '',
507
+ renameFolderInputValue: '',
508
+ originalFileBeingRenamed: '',
509
+ originalFolderBeingRenamed: '',
510
+ newFileParentPath: '',
511
+ newFolderParentPath: '',
512
+ fileStructure,
513
+ },
514
+ editorSnapshot: {
515
+ isEditorContextMenuOpen: false,
516
+ editors: []
517
+ },
518
+ terminalSnapshot: {
519
+ terminals: []
520
+ },
521
+ mouseSnapshot: {
522
+ location: 'editor',
523
+ currentHoveredFileName: '',
524
+ currentHoveredFolderName: '',
525
+ currentHoveredEditorTabFileName: '',
526
+ x: 0,
527
+ y: 0,
528
+ timestamp: 0,
529
+ type: 'move',
530
+ buttonStates: {
531
+ left: false,
532
+ right: false,
533
+ middle: false,
534
+ },
535
+ scrollPosition: {
536
+ x: 0,
537
+ y: 0
538
+ },
539
+ },
540
+ authorSnapshot: {
541
+ authors: [
542
+ { currentSpeechCaption: '' }
543
+ ]
544
+ }
545
+ }
546
+ };
547
+ // return the project as a string
548
+ return JSON.stringify(project);
549
+ };
550
+
551
+ const getInitialLessonStateFromGitHubRepo = async (repoUrl) => {
552
+ // build file structure from files and content from the file system adapter
553
+ const fileStructure = await buildFileStructureFromGitHub(repoUrl);
261
554
  // build the project using the file structure
262
555
  const project = {
263
556
  id: "",
@@ -546,7 +839,7 @@ const setCurrentActions = (actions) => {
546
839
  `);
547
840
  // Convert actions to JSON string and store
548
841
  const actionsJson = JSON.stringify(actions);
549
- stmt.run('current_actions', actionsJson);
842
+ stmt.run(CURRENT_ACTIONS_KEY, actionsJson);
550
843
  db.close();
551
844
  return `Successfully stored ${actions.length} actions in local database at ${dbPath}`;
552
845
  }
@@ -555,6 +848,90 @@ const setCurrentActions = (actions) => {
555
848
  }
556
849
  };
557
850
 
851
+ // sets the current lesson to the key value sqlite database
852
+ const setCurrentLesson = (lesson) => {
853
+ try {
854
+ // Create database directory if it doesn't exist
855
+ const homeDir = os.homedir();
856
+ const dbDir = path.join(homeDir, '.codevideo');
857
+ if (!fs.existsSync(dbDir)) {
858
+ fs.mkdirSync(dbDir, { recursive: true });
859
+ }
860
+ const dbPath = path.join(dbDir, 'codevideo.db');
861
+ // Open or create database
862
+ const db = new Database(dbPath);
863
+ // Create table if it doesn't exist
864
+ db.exec(`
865
+ CREATE TABLE IF NOT EXISTS key_value_store (
866
+ key TEXT PRIMARY KEY,
867
+ value TEXT NOT NULL,
868
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
869
+ )
870
+ `);
871
+ // Prepare statement to insert or replace the current_lesson
872
+ const stmt = db.prepare(`
873
+ INSERT OR REPLACE INTO key_value_store (key, value, updated_at)
874
+ VALUES (?, ?, CURRENT_TIMESTAMP)
875
+ `);
876
+ // Convert lesson to JSON string and store
877
+ const lessonJson = JSON.stringify(lesson);
878
+ stmt.run(CURRENT_LESSON_KEY, lessonJson);
879
+ db.close();
880
+ return `Successfully stored lesson "${lesson.name}" (ID: ${lesson.id}) in local database at ${dbPath}`;
881
+ }
882
+ catch (error) {
883
+ return `Error storing lesson: ${error.message}`;
884
+ }
885
+ };
886
+
887
+ // retrieves the current lesson from the key value sqlite database
888
+ const getCurrentLesson = () => {
889
+ try {
890
+ const homeDir = os.homedir();
891
+ const dbPath = path.join(homeDir, '.codevideo', 'codevideo.db');
892
+ // Check if database file exists
893
+ if (!fs.existsSync(dbPath)) {
894
+ return null;
895
+ }
896
+ // Open database
897
+ const db = new Database(dbPath);
898
+ // Check if table exists
899
+ const tableExists = db.prepare(`
900
+ SELECT name FROM sqlite_master
901
+ WHERE type='table' AND name='key_value_store'
902
+ `).get();
903
+ if (!tableExists) {
904
+ db.close();
905
+ return null;
906
+ }
907
+ // Retrieve current_lesson
908
+ const stmt = db.prepare(`
909
+ SELECT value FROM key_value_store
910
+ WHERE key = ?
911
+ `);
912
+ const result = stmt.get(CURRENT_LESSON_KEY);
913
+ db.close();
914
+ if (!result) {
915
+ return null;
916
+ }
917
+ // Parse JSON back to lesson object
918
+ const lesson = JSON.parse(result.value);
919
+ return lesson;
920
+ }
921
+ catch (error) {
922
+ console.error('Error retrieving lesson:', error);
923
+ return null;
924
+ }
925
+ };
926
+ // Helper function that returns lesson or throws descriptive error
927
+ const getCurrentLessonOrThrow = () => {
928
+ const lesson = getCurrentLesson();
929
+ if (!lesson) {
930
+ throw new Error('No lesson found in storage. Please use codevideo_set_current_lesson first to store a lesson, then try again.');
931
+ }
932
+ return lesson;
933
+ };
934
+
558
935
  // calls the markdown generator from the codevideo-exporters package
559
936
  const makeBlogPost = (actions) => {
560
937
  // Use provided actions or retrieve from storage
@@ -585,32 +962,63 @@ const makeHTMLWebPage = async (actions) => {
585
962
  return htmlContent;
586
963
  };
587
964
 
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) => {
965
+ const makeVideoFromActions = async (actions) => {
966
+ // attempt to read CLI path from env varaible PATH_TO_CODEVIDEO_CLI - if PATH_TO_CODEVIDEO_CLI is not set return error message
967
+ // const cliExePath = process.env.PATH_TO_CODEVIDEO_CLI;
968
+ // hardcoded now for demo
969
+ const cliExePath = "/Users/chris/enterprise/codevideo/codevideo-cli/codevideo";
970
+ // TODO: reactivate getBinaryPath from the go binaries that are downloaded from releases github and packaged here
971
+ // we will likely have signing issues on macOS, so we will need to sign the binary ourselves
972
+ // const cliExePath = getBinaryPath();
973
+ // if (!cliExePath) {
974
+ // return {
975
+ // outputPath: '',
976
+ // stdout: '',
977
+ // stderr: '',
978
+ // error: new Error('PATH_TO_CODEVIDEO_CLI environment variable is not set. Please set it to the path of the codevideo-cli executable.')
979
+ // };
980
+ // }
981
+ // // if it is set, check if it exists
982
+ // if (!path.isAbsolute(cliExePath)) {
983
+ // return {
984
+ // outputPath: '',
985
+ // stdout: '',
986
+ // stderr: '',
987
+ // error: new Error('PATH_TO_CODEVIDEO_CLI environment variable is not an absolute path. Please set it to the absolute path of the codevideo-cli executable.')
988
+ // };
989
+ // }
990
+ // // check if the path exists
991
+ // if (!fs.existsSync(cliExePath)) {
992
+ // return {
993
+ // outputPath: '',
994
+ // stdout: '',
995
+ // stderr: '',
996
+ // error: new Error(`PATH_TO_CODEVIDEO_CLI environment variable is set to a path that does not exist: ${cliExePath}. Please set it to the path of the codevideo-cli executable.`)
997
+ // };
998
+ // }
999
+ // // check if the path is a file
1000
+ // if (!fs.statSync(cliExePath).isFile()) {
1001
+ // return {
1002
+ // outputPath: '',
1003
+ // stdout: '',
1004
+ // stderr: '',
1005
+ // error: new Error(`PATH_TO_CODEVIDEO_CLI environment variable is set to a path that is not a file: ${cliExePath}. Please set it to the path of the codevideo-cli executable.`)
1006
+ // };
1007
+ // }
1008
+ // // check if the path is executable
1009
+ // try {
1010
+ // fs.accessSync(cliExePath, fs.constants.F_OK | fs.constants.X_OK);
1011
+ // } catch (error) {
1012
+ // return {
1013
+ // outputPath: '',
1014
+ // stdout: '',
1015
+ // stderr: '',
1016
+ // error: new Error(`PATH_TO_CODEVIDEO_CLI environment variable is set to a path that is not executable: ${cliExePath}. Please set it to the path of the codevideo-cli executable.`)
1017
+ // };
1018
+ // }
1019
+ // derive the directory path from the executable path
1020
+ const cliDirPath = path.dirname(cliExePath);
612
1021
  try {
613
- const binaryPath = getBinaryPath(); // Use bundled binary instead of PATH_TO_CODEVIDEO_CLI
614
1022
  // Use provided actions or retrieve from storage
615
1023
  const actionsToUse = actions || getCurrentActionsOrThrow();
616
1024
  // Create JSON string directly
@@ -621,16 +1029,109 @@ const makeVideo = async (actions) => {
621
1029
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
622
1030
  const outputFile = path.join(desktopDir, `codevideo-${timestamp}.mp4`);
623
1031
  // 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()
1032
+ const child = spawn(cliExePath, ['run', '.', '-p', actionsJson, '-o', outputFile, '--open'], {
1033
+ cwd: cliDirPath,
1034
+ detached: true,
1035
+ stdio: 'ignore' // Detach stdio streams from parent
1036
+ });
1037
+ // Unreference the child process, allowing the parent to exit independently
1038
+ child.unref();
1039
+ // Return immediately with status message
1040
+ return {
1041
+ outputPath: outputFile,
1042
+ stdout: `Video generation job with ${actionsToUse.length} actions has 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 - this could take a while depending on how many actions there are.`,
1043
+ stderr: ''
1044
+ };
1045
+ }
1046
+ catch (error) {
1047
+ console.error('Error starting video generation:', error);
1048
+ return {
1049
+ outputPath: '',
1050
+ stdout: '',
1051
+ stderr: `Failed to start video generation: ${error.message}`,
1052
+ error
1053
+ };
1054
+ }
1055
+ };
1056
+
1057
+ const makeVideoFromLesson = async (lesson) => {
1058
+ // attempt to read CLI path from env varaible PATH_TO_CODEVIDEO_CLI - if PATH_TO_CODEVIDEO_CLI is not set return error message
1059
+ // const cliExePath = process.env.PATH_TO_CODEVIDEO_CLI;
1060
+ // hardcoded now for demo
1061
+ const cliExePath = "/Users/chris/enterprise/codevideo/codevideo-cli/codevideo";
1062
+ // TODO: reactivate getBinaryPath from the go binaries that are downloaded from releases github and packaged here
1063
+ // we will likely have signing issues on macOS, so we will need to sign the binary ourselves
1064
+ // const cliExePath = getBinaryPath();
1065
+ // if (!cliExePath) {
1066
+ // return {
1067
+ // outputPath: '',
1068
+ // stdout: '',
1069
+ // stderr: '',
1070
+ // error: new Error('PATH_TO_CODEVIDEO_CLI environment variable is not set. Please set it to the path of the codevideo-cli executable.')
1071
+ // };
1072
+ // }
1073
+ // // if it is set, check if it exists
1074
+ // if (!path.isAbsolute(cliExePath)) {
1075
+ // return {
1076
+ // outputPath: '',
1077
+ // stdout: '',
1078
+ // stderr: '',
1079
+ // error: new Error('PATH_TO_CODEVIDEO_CLI environment variable is not an absolute path. Please set it to the absolute path of the codevideo-cli executable.')
1080
+ // };
1081
+ // }
1082
+ // // check if the path exists
1083
+ // if (!fs.existsSync(cliExePath)) {
1084
+ // return {
1085
+ // outputPath: '',
1086
+ // stdout: '',
1087
+ // stderr: '',
1088
+ // error: new Error(`PATH_TO_CODEVIDEO_CLI environment variable is set to a path that does not exist: ${cliExePath}. Please set it to the path of the codevideo-cli executable.`)
1089
+ // };
1090
+ // }
1091
+ // // check if the path is a file
1092
+ // if (!fs.statSync(cliExePath).isFile()) {
1093
+ // return {
1094
+ // outputPath: '',
1095
+ // stdout: '',
1096
+ // stderr: '',
1097
+ // error: new Error(`PATH_TO_CODEVIDEO_CLI environment variable is set to a path that is not a file: ${cliExePath}. Please set it to the path of the codevideo-cli executable.`)
1098
+ // };
1099
+ // }
1100
+ // // check if the path is executable
1101
+ // try {
1102
+ // fs.accessSync(cliExePath, fs.constants.F_OK | fs.constants.X_OK);
1103
+ // } catch (error) {
1104
+ // return {
1105
+ // outputPath: '',
1106
+ // stdout: '',
1107
+ // stderr: '',
1108
+ // error: new Error(`PATH_TO_CODEVIDEO_CLI environment variable is set to a path that is not executable: ${cliExePath}. Please set it to the path of the codevideo-cli executable.`)
1109
+ // };
1110
+ // }
1111
+ // derive the directory path from the executable path
1112
+ const cliDirPath = path.dirname(cliExePath);
1113
+ try {
1114
+ // Use provided lesson or retrieve from storage
1115
+ const lessonToUse = lesson || getCurrentLessonOrThrow();
1116
+ // Create JSON string directly
1117
+ const lessonJson = JSON.stringify(lessonToUse);
1118
+ // Create output file on desktop with timestamp
1119
+ const homeDir = os.homedir();
1120
+ const desktopDir = path.join(homeDir, 'Desktop');
1121
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1122
+ const outputFile = path.join(desktopDir, `codevideo-${timestamp}.mp4`);
1123
+ // Fire-and-forget: Start the video generation process in the background
1124
+ const child = spawn(cliExePath, ['run', '.', '-p', lessonJson, '-o', outputFile, '--open'], {
1125
+ cwd: cliDirPath,
1126
+ detached: true,
1127
+ stdio: 'ignore' // Detach stdio streams from parent
627
1128
  });
628
1129
  // Unreference the child process, allowing the parent to exit independently
629
1130
  child.unref();
630
1131
  // Return immediately with status message
631
1132
  return {
632
1133
  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.`,
1134
+ stdout: `Video generation job with lesson has 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 - this could take a while depending on the complexity of the lesson.`,
634
1135
  stderr: ''
635
1136
  };
636
1137
  }
@@ -674,7 +1175,7 @@ const getActionNames = () => {
674
1175
  return AllActionStrings.join(", ");
675
1176
  };
676
1177
 
677
- const keywordsToExampleMap = [
1178
+ const keywordsToExampleActionsMap = [
678
1179
  {
679
1180
  keywords: ["console log", "javascript", "js", "log"],
680
1181
  actions: [
@@ -997,19 +1498,19 @@ const keywordsToExampleMap = [
997
1498
  ];
998
1499
  // returns a JSON representation of an example lesson based on keywords
999
1500
  // the LLM can use this to learn real example lessons created by humans
1000
- const getExampleLesson = (keyword) => {
1501
+ const getExampleActionsArrayByKeyword = (keyword) => {
1001
1502
  const exampleJSONs = [];
1002
1503
  if (Array.isArray(keyword)) {
1003
1504
  // if the keyword is an array, loop over all keywords and get the example lesson for each one
1004
1505
  for (var i = 0; i < keyword.length; i++) {
1005
- const exampleJSON = getExampleLesson(keyword[i]);
1506
+ const exampleJSON = getExampleActionsArrayByKeyword(keyword[i]);
1006
1507
  exampleJSONs.push(exampleJSON);
1007
1508
  }
1008
1509
  return exampleJSONs.join(", ");
1009
1510
  }
1010
1511
  else {
1011
1512
  // if the keyword is a string, get the example lesson for that keyword
1012
- const exampleJSON = getSingleExampleLesson(keyword);
1513
+ const exampleJSON = getSingleExampleActionsArray(keyword);
1013
1514
  if (exampleJSON) {
1014
1515
  return exampleJSON;
1015
1516
  }
@@ -1017,10 +1518,10 @@ const getExampleLesson = (keyword) => {
1017
1518
  // if no keywords match, return an empty string
1018
1519
  return "";
1019
1520
  };
1020
- const getSingleExampleLesson = (keyword) => {
1521
+ const getSingleExampleActionsArray = (keyword) => {
1021
1522
  // 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];
1523
+ for (var i = 0; i < keywordsToExampleActionsMap.length; i++) {
1524
+ const keywordMap = keywordsToExampleActionsMap[i];
1024
1525
  if (keywordMap.keywords.includes(keyword)) {
1025
1526
  return JSON.stringify(keywordMap.actions);
1026
1527
  }
@@ -1029,344 +1530,816 @@ const getSingleExampleLesson = (keyword) => {
1029
1530
  return "";
1030
1531
  };
1031
1532
 
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)
1533
+ const keywordsToExampleLessonsMap = [
1534
+ {
1535
+ keywords: ["go", "golang", "explore codebase", "existing codebase", "go cli tool"],
1536
+ lesson: {
1537
+ "id": "",
1538
+ "name": "",
1539
+ "description": "",
1540
+ "actions": [
1541
+ {
1542
+ "name": "author-speak-before",
1543
+ "value": "Hi there! Let's explore this interesting Golang repository that helps track GitLab contributions. This tool analyzes user activity across different time periods and produces contribution statistics."
1544
+ },
1545
+ {
1546
+ "name": "mouse-move-file-explorer",
1547
+ "value": "1"
1548
+ },
1549
+ {
1550
+ "name": "mouse-move-file-explorer-file",
1551
+ "value": "main.go"
1552
+ },
1553
+ {
1554
+ "name": "mouse-left-click",
1555
+ "value": "1"
1556
+ },
1557
+ {
1558
+ "name": "author-speak-before",
1559
+ "value": "The main.go file is the entry point for the application. It defines the core functionality for tracking GitLab contributions."
1560
+ },
1561
+ {
1562
+ "name": "mouse-move-to-coordinates-percent",
1563
+ "value": "50,20"
1564
+ },
1565
+ {
1566
+ "name": "author-speak-before",
1567
+ "value": "This struct stores contribution statistics for a user - tracking both commits and merge requests across different time periods."
1568
+ },
1569
+ {
1570
+ "name": "author-speak-before",
1571
+ "value": "The main function initializes the application by loading environment variables, getting the GitLab token, and reading usernames from a file."
1572
+ },
1573
+ {
1574
+ "name": "mouse-move-file-explorer-file",
1575
+ "value": "usernames.txt"
1576
+ },
1577
+ {
1578
+ "name": "mouse-left-click",
1579
+ "value": "1"
1580
+ },
1581
+ {
1582
+ "name": "author-speak-before",
1583
+ "value": "The usernames.txt file contains the list of GitLab users whose contributions we want to track."
1584
+ },
1585
+ {
1586
+ "name": "mouse-move-file-explorer-file",
1587
+ "value": "main.go"
1588
+ },
1589
+ {
1590
+ "name": "mouse-left-click",
1591
+ "value": "1"
1592
+ },
1593
+ {
1594
+ "name": "file-explorer-open-file",
1595
+ "value": "main.go"
1596
+ },
1597
+ {
1598
+ "name": "author-speak-before",
1599
+ "value": "This function calculates contribution statistics for a single user. It defines time windows for different periods - daily, weekly, monthly, and yearly."
1600
+ },
1601
+ {
1602
+ "name": "author-speak-before",
1603
+ "value": "The GetUserActivity function connects to the GitLab API to fetch user events within a specified time period."
1604
+ },
1605
+ {
1606
+ "name": "author-speak-before",
1607
+ "value": "The printResults function formats and displays the contribution statistics, including calculating metrics like average commits per merge request."
1608
+ },
1609
+ {
1610
+ "name": "mouse-move-file-explorer",
1611
+ "value": "1"
1612
+ },
1613
+ {
1614
+ "name": "mouse-move-file-explorer-folder",
1615
+ "value": "http_helper"
1616
+ },
1617
+ {
1618
+ "name": "mouse-left-click",
1619
+ "value": "1"
1620
+ },
1621
+ {
1622
+ "name": "file-explorer-expand-folder",
1623
+ "value": "http_helper"
1624
+ },
1625
+ {
1626
+ "name": "mouse-move-file-explorer-file",
1627
+ "value": "http_helper/http_helper.go"
1628
+ },
1629
+ {
1630
+ "name": "mouse-left-click",
1631
+ "value": "1"
1632
+ },
1633
+ {
1634
+ "name": "file-explorer-open-file",
1635
+ "value": "http_helper/http_helper.go"
1636
+ },
1637
+ {
1638
+ "name": "author-speak-before",
1639
+ "value": "The project has a dedicated HTTP helper module that handles API requests to GitLab."
1640
+ },
1641
+ {
1642
+ "name": "editor-highlight-code",
1643
+ "value": "func MakeHTTPRequest(fullUrl string, httpMethod string, headers map[string]string, queryParameters url.Values, body io.Reader) ([]byte, error) {"
1644
+ },
1645
+ {
1646
+ "name": "author-speak-before",
1647
+ "value": "This function handles standard HTTP requests, while the function below uses generics for typed responses."
1648
+ },
1649
+ {
1650
+ "name": "editor-highlight-code",
1651
+ "value": "func MakeHTTPRequestGeneric[T any](fullUrl string, httpMethod string, headers map[string]string, queryParameters url.Values, body io.Reader, responseType T) (T, error) {"
1652
+ },
1653
+ {
1654
+ "name": "author-speak-before",
1655
+ "value": "MakeHTTPRequestGeneric is a modern Go function using generics to handle typed API responses, making it easier to work with the GitLab API."
1656
+ },
1657
+ {
1658
+ "name": "mouse-move-file-explorer-file",
1659
+ "value": "main_test.go"
1660
+ },
1661
+ {
1662
+ "name": "mouse-left-click",
1663
+ "value": "1"
1664
+ },
1665
+ {
1666
+ "name": "file-explorer-open-file",
1667
+ "value": "main_test.go"
1668
+ },
1669
+ {
1670
+ "name": "author-speak-before",
1671
+ "value": "The project also includes tests to verify the functionality of the GitLab API interactions."
1672
+ },
1673
+ {
1674
+ "name": "mouse-move-file-explorer-file",
1675
+ "value": ".env"
1676
+ },
1677
+ {
1678
+ "name": "mouse-left-click",
1679
+ "value": "1"
1680
+ },
1681
+ {
1682
+ "name": "file-explorer-open-file",
1683
+ "value": ".env"
1684
+ },
1685
+ {
1686
+ "name": "author-speak-before",
1687
+ "value": "The .env file stores the GitLab token needed to authenticate with the API."
1688
+ },
1689
+ {
1690
+ "name": "mouse-move-file-explorer-file",
1691
+ "value": "go.mod"
1692
+ },
1693
+ {
1694
+ "name": "mouse-left-click",
1695
+ "value": "1"
1696
+ },
1697
+ {
1698
+ "name": "file-explorer-open-file",
1699
+ "value": "go.mod"
1700
+ },
1701
+ {
1702
+ "name": "author-speak-before",
1703
+ "value": "The go.mod file lists the project dependencies, including the go-gitlab library for API interaction and godotenv for environment variable loading."
1704
+ },
1705
+ {
1706
+ "name": "mouse-move-file-explorer-file",
1707
+ "value": "main.go"
1708
+ },
1709
+ {
1710
+ "name": "mouse-left-click",
1711
+ "value": "1"
1712
+ },
1713
+ {
1714
+ "name": "file-explorer-open-file",
1715
+ "value": "main.go"
1716
+ },
1717
+ {
1718
+ "name": "author-speak-after",
1719
+ "value": "To summarize, this Go repository is a GitLab contribution analyzer that fetches user activity data from GitLab's API. It tracks commits and merge requests across multiple time periods, calculates stats, and outputs nicely formatted results with progress bars. It's designed to help track team productivity and identify top contributors. The code is well-structured with separate modules for HTTP requests and includes proper error handling and testing."
1720
+ }
1721
+ ],
1722
+ "initialSnapshot": {
1723
+ "isUnsavedChangesDialogOpen": false,
1724
+ "unsavedFileName": "",
1725
+ "fileExplorerSnapshot": {
1726
+ "isFileExplorerContextMenuOpen": false,
1727
+ "isFileContextMenuOpen": false,
1728
+ "isFolderContextMenuOpen": false,
1729
+ "isNewFileInputVisible": false,
1730
+ "isNewFolderInputVisible": false,
1731
+ "isRenameFileInputVisible": false,
1732
+ "isRenameFolderInputVisible": false,
1733
+ "newFileInputValue": "",
1734
+ "newFolderInputValue": "",
1735
+ "renameFileInputValue": "",
1736
+ "renameFolderInputValue": "",
1737
+ "originalFileBeingRenamed": "",
1738
+ "originalFolderBeingRenamed": "",
1739
+ "newFileParentPath": "",
1740
+ "newFolderParentPath": "",
1741
+ "fileStructure": {
1742
+ ".env": {
1743
+ "type": "file",
1744
+ "content": "GITLAB_TOKEN=glpat-uShSYUHwpUcDxfkGcoNL",
1745
+ "language": "plaintext",
1746
+ "caretPosition": {
1747
+ "row": 0,
1748
+ "col": 0
1749
+ }
1750
+ },
1751
+ ".gitignore": {
1752
+ "type": "file",
1753
+ "content": ".env",
1754
+ "language": "plaintext",
1755
+ "caretPosition": {
1756
+ "row": 0,
1757
+ "col": 0
1758
+ }
1759
+ },
1760
+ "go.mod": {
1761
+ "type": "file",
1762
+ "content": "module solve.ch/solvers\n\ngo 1.23.0\n\nrequire github.com/xanzy/go-gitlab v0.115.0\n\nrequire (\n\tgithub.com/golang/protobuf v1.5.3 // indirect\n\tgithub.com/google/go-querystring v1.1.0 // indirect\n\tgithub.com/hashicorp/go-cleanhttp v0.5.2 // indirect\n\tgithub.com/hashicorp/go-retryablehttp v0.7.7 // indirect\n\tgithub.com/joho/godotenv v1.5.1\n\tgitlab.com/gitlab-org/api/client-go v0.120.0\n\tgolang.org/x/net v0.8.0 // indirect\n\tgolang.org/x/oauth2 v0.24.0 // indirect\n\tgolang.org/x/time v0.8.0 // indirect\n\tgoogle.golang.org/appengine v1.6.7 // indirect\n\tgoogle.golang.org/protobuf v1.29.1 // indirect\n)\n",
1763
+ "language": "plaintext",
1764
+ "caretPosition": {
1765
+ "row": 0,
1766
+ "col": 0
1767
+ }
1768
+ },
1769
+ "go.sum": {
1770
+ "type": "file",
1771
+ "content": "github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=\ngithub.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=\ngithub.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=\ngithub.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=\ngithub.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=\ngithub.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=\ngithub.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=\ngithub.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8=\ngithub.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M=\ngitlab.com/gitlab-org/api/client-go v0.120.0 h1:geCJjojDXxWVmUcTxPcOUCenAWElWB5dVfX3HJGeAMc=\ngitlab.com/gitlab-org/api/client-go v0.120.0/go.mod h1:ygHmS3AU3TpvK+AC6DYO1QuAxLlv6yxYK+/Votr/WFQ=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=\ngolang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=\ngolang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=\ngolang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=\ngolang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=\ngolang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=\ngolang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=\ngolang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=\ngoogle.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM=\ngoogle.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\n",
1772
+ "language": "plaintext",
1773
+ "caretPosition": {
1774
+ "row": 0,
1775
+ "col": 0
1776
+ }
1777
+ },
1778
+ "http_helper": {
1779
+ "type": "directory",
1780
+ "content": "",
1781
+ "collapsed": false,
1782
+ "children": {
1783
+ "http_helper.go": {
1784
+ "type": "file",
1785
+ "content": "package http_helper\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n)\n\n// in the case of GET, the parameter queryParameters is transferred to the URL as query parameters\n// in the case of POST, the parameter body, an io.Reader, is used\nfunc MakeHTTPRequest(fullUrl string, httpMethod string, headers map[string]string, queryParameters url.Values, body io.Reader) ([]byte, error) {\n\tclient := http.Client{}\n\tu, err := url.Parse(fullUrl)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// if it's a GET, we need to append the query parameters.\n\tif httpMethod == \"GET\" {\n\t\tq := u.Query()\n\n\t\tfor k, v := range queryParameters {\n\t\t\t// this depends on the type of api, you may need to do it for each of v\n\t\t\tq.Set(k, strings.Join(v, \",\"))\n\t\t}\n\t\t// set the query to the encoded parameters\n\t\tu.RawQuery = q.Encode()\n\t}\n\n\t// regardless of GET or POST, we can safely add the body\n\treq, err := http.NewRequest(httpMethod, u.String(), body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// for each header passed, add the header value to the request\n\tfor k, v := range headers {\n\t\treq.Header.Set(k, v)\n\t}\n\n\t// optional: log the request for easier stack tracing\n\tlog.Printf(\"%s %s\\n\", httpMethod, req.URL.String())\n\n\t// finally, do the request\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif res == nil {\n\t\treturn nil, fmt.Errorf(\"error: calling %s returned empty response\", u.String())\n\t}\n\n\tresponseData, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer res.Body.Close()\n\n\tif res.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"error calling %s:\\nstatus: %s\\nresponseData: %s\", u.String(), res.Status, responseData)\n\t}\n\n\treturn responseData, nil\n}\n\nfunc MakeHTTPRequestGeneric[T any](fullUrl string, httpMethod string, headers map[string]string, queryParameters url.Values, body io.Reader, responseType T) (T, error) {\n\tclient := http.Client{}\n\tu, err := url.Parse(fullUrl)\n\tif err != nil {\n\t\treturn responseType, err\n\t}\n\n\t// if it's a GET, we need to append the query parameters.\n\tif httpMethod == \"GET\" {\n\t\tq := u.Query()\n\n\t\tfor k, v := range queryParameters {\n\t\t\t// this depends on the type of api, you may need to do it for each of v\n\t\t\tq.Set(k, strings.Join(v, \",\"))\n\t\t}\n\t\t// set the query to the encoded parameters\n\t\tu.RawQuery = q.Encode()\n\t}\n\n\t// regardless of GET or POST, we can safely add the body\n\treq, err := http.NewRequest(httpMethod, u.String(), body)\n\tif err != nil {\n\t\treturn responseType, err\n\t}\n\n\t// for each header passed, add the header value to the request\n\tfor k, v := range headers {\n\t\treq.Header.Set(k, v)\n\t}\n\n\t// optional: log the request for easier stack tracing\n\tlog.Printf(\"%s %s\\n\", httpMethod, req.URL.String())\n\n\t// finally, do the request\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\treturn responseType, err\n\t}\n\n\tif res == nil {\n\t\treturn responseType, fmt.Errorf(\"error: calling %s returned empty response\", u.String())\n\t}\n\n\tresponseData, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn responseType, err\n\t}\n\n\tdefer res.Body.Close()\n\n\tif res.StatusCode != http.StatusOK {\n\t\treturn responseType, fmt.Errorf(\"error calling %s:\\nstatus: %s\\nresponseData: %s\", u.String(), res.Status, responseData)\n\t}\n\n\tvar responseObject T\n\terr = json.Unmarshal(responseData, &responseObject)\n\n\tif err != nil {\n\t\tlog.Printf(\"error unmarshaling response: %+v\", err)\n\t\treturn responseType, err\n\t}\n\n\treturn responseObject, nil\n}\n",
1786
+ "language": "Go",
1787
+ "caretPosition": {
1788
+ "row": 0,
1789
+ "col": 0
1790
+ }
1791
+ }
1792
+ }
1793
+ },
1794
+ "main.go": {
1795
+ "type": "file",
1796
+ "content": "package main\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/joho/godotenv\"\n\t\"solve.ch/solvers/http_helper\"\n)\n\ntype ContributionStats struct {\n\tUsername string\n\tDailyCommits int\n\tWeeklyCommits int\n\tMonthlyCommits int\n\tYearlyCommits int\n\tDailyMRs int\n\tWeeklyMRs int\n\tMonthlyMRs int\n\tYearlyMRs int\n\t// New field to store total pages fetched from GitLab\n\tPagesFetched int\n}\n\ntype TimePeriod struct {\n\tStart time.Time\n\tEnd time.Time\n}\n\ntype GitLabAuthor struct {\n\tID int `json:\"id\"`\n\tUsername string `json:\"username\"`\n\tName string `json:\"name\"`\n\tState string `json:\"state\"`\n\tAvatarURL string `json:\"avatar_url\"`\n\tWebURL string `json:\"web_url\"`\n}\n\ntype GitLabPushData struct {\n\tCommitCount int `json:\"commit_count\"`\n\tAction string `json:\"action\"`\n\tRefType string `json:\"ref_type\"`\n\tCommitFrom string `json:\"commit_from\"`\n\tCommitTo string `json:\"commit_to\"`\n\tRef string `json:\"ref\"`\n\tCommitTitle string `json:\"commit_title\"`\n\tRefCount *int `json:\"ref_count\"`\n}\n\ntype GitLabEvent struct {\n\tID int `json:\"id\"`\n\tProjectID int `json:\"project_id\"`\n\tActionName string `json:\"action_name\"`\n\tTargetID *int `json:\"target_id\"`\n\tTargetIID *int `json:\"target_iid\"`\n\tTargetType *string `json:\"target_type\"`\n\tAuthorID int `json:\"author_id\"`\n\tTargetTitle *string `json:\"target_title\"`\n\tCreatedAt time.Time `json:\"created_at\"`\n\tAuthor GitLabAuthor `json:\"author\"`\n\tPushData *GitLabPushData `json:\"push_data\"`\n}\n\nfunc main() {\n\t// Load .env file\n\terr := godotenv.Load()\n\tif err != nil {\n\t\tlog.Println(\"Warning: Error loading .env file:\", err)\n\t}\n\n\t// Replace with your GitLab token\n\ttoken := os.Getenv(\"GITLAB_TOKEN\")\n\tif token == \"\" {\n\t\tlog.Fatal(\"GITLAB_TOKEN environment variable not set\")\n\t}\n\n\t// Read usernames from file\n\tusernames, err := readUsernames(\"usernames.txt\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to read usernames: %v\", err)\n\t}\n\n\t// Get contribution stats for each user\n\tstats := make([]ContributionStats, 0)\n\tfor _, username := range usernames {\n\t\tstat, err := getContributionStats(token, username)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Warning: Failed to get stats for %s: %v\", username, err)\n\t\t\tcontinue\n\t\t}\n\t\tstats = append(stats, stat)\n\t}\n\n\t// Sort by yearly commits (could be modified to use different metrics)\n\tsort.Slice(stats, func(i, j int) bool {\n\t\treturn stats[i].YearlyCommits > stats[j].YearlyCommits\n\t})\n\n\t// Print results with normalized bars\n\tprintResults(stats)\n}\n\nfunc readUsernames(filename string) ([]string, error) {\n\tfile, err := os.Open(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer file.Close()\n\n\tvar usernames []string\n\tscanner := bufio.NewScanner(file)\n\tfor scanner.Scan() {\n\t\tusername := strings.TrimSpace(scanner.Text())\n\t\tif username != \"\" {\n\t\t\tusernames = append(usernames, username)\n\t\t}\n\t}\n\treturn usernames, scanner.Err()\n}\n\nfunc getContributionStats(token, username string) (ContributionStats, error) {\n\tstats := ContributionStats{Username: username}\n\n\t// Define time windows for daily, weekly, monthly, yearly\n\ttodayStart := time.Now().Truncate(24 * time.Hour)\n\ttodayEnd := todayStart.Add(24 * time.Hour)\n\n\t// last 7 days (including today)\n\tweekStart := todayStart.AddDate(0, 0, -6)\n\tweekEnd := todayEnd\n\n\t// last 1 month\n\tmonthStart := todayStart.AddDate(0, -1, 0)\n\tmonthEnd := todayEnd\n\n\t// last 1 year\n\tyearStart := todayStart.AddDate(-1, 0, 0)\n\tyearEnd := todayEnd\n\n\tperiods := map[string]TimePeriod{\n\t\t\"daily\": {\n\t\t\tStart: todayStart,\n\t\t\tEnd: todayEnd,\n\t\t},\n\t\t\"weekly\": {\n\t\t\tStart: weekStart,\n\t\t\tEnd: weekEnd,\n\t\t},\n\t\t\"monthly\": {\n\t\t\tStart: monthStart,\n\t\t\tEnd: monthEnd,\n\t\t},\n\t\t\"yearly\": {\n\t\t\tStart: yearStart,\n\t\t\tEnd: yearEnd,\n\t\t},\n\t}\n\n\t// We'll track how many pages we fetched total for each user\n\ttotalPages := 0\n\n\t// Get user's events for each time period\n\tfor period, timeRange := range periods {\n\t\tcommits, mrs, pages, err := GetUserActivity(token, username, timeRange.Start, timeRange.End)\n\t\tif err != nil {\n\t\t\treturn stats, err\n\t\t}\n\t\t// Accumulate total page count\n\t\ttotalPages += pages\n\n\t\tswitch period {\n\t\tcase \"daily\":\n\t\t\tstats.DailyCommits = commits\n\t\t\tstats.DailyMRs = mrs\n\t\tcase \"weekly\":\n\t\t\tstats.WeeklyCommits = commits\n\t\t\tstats.WeeklyMRs = mrs\n\t\tcase \"monthly\":\n\t\t\tstats.MonthlyCommits = commits\n\t\t\tstats.MonthlyMRs = mrs\n\t\tcase \"yearly\":\n\t\t\tstats.YearlyCommits = commits\n\t\t\tstats.YearlyMRs = mrs\n\t\t}\n\t}\n\n\t// Store the total pages fetched in stats\n\tstats.PagesFetched = totalPages\n\n\treturn stats, nil\n}\n\n// GetUserActivity returns the total commits, MRs, and the number of pages fetched\nfunc GetUserActivity(token, username string, start, end time.Time) (commits int, mrs int, totalPages int, err error) {\n\tbaseURL := \"https://git.solve.ch/api/v4/users\"\n\t// We will iterate over pages, so exclude &page from the base URL:\n\tfullURL := fmt.Sprintf(\n\t\t\"%s/%s/events?after=%s&before=%s&per_page=100\",\n\t\tbaseURL,\n\t\tusername,\n\t\tstart.Format(\"2006-01-02\"),\n\t\tend.Format(\"2006-01-02\"),\n\t)\n\n\theaders := map[string]string{\n\t\t\"PRIVATE-TOKEN\": token,\n\t}\n\n\tvar allEvents []GitLabEvent\n\tpage := 1\n\n\tfor {\n\t\t// Construct URL for the current page\n\t\tpagedURL := fmt.Sprintf(\"%s&page=%d\", fullURL, page)\n\n\t\tvar tempEvents []GitLabEvent\n\t\ttempEvents, err = http_helper.MakeHTTPRequestGeneric(\n\t\t\tpagedURL,\n\t\t\t\"GET\",\n\t\t\theaders,\n\t\t\tnil,\n\t\t\tnil,\n\t\t\ttempEvents,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn 0, 0, 0, fmt.Errorf(\"failed to get user events (page %d): %v\", page, err)\n\t\t}\n\n\t\t// If no events, we've reached the end\n\t\tif len(tempEvents) == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\t// Accumulate events\n\t\tallEvents = append(allEvents, tempEvents...)\n\t\tpage++\n\t}\n\n\t// The page - 1 is how many pages we actually fetched\n\ttotalPages = page - 1\n\n\t// Count commits and MRs\n\tfor _, event := range allEvents {\n\t\tswitch {\n\t\tcase event.ActionName == \"pushed to\" || event.ActionName == \"pushed new\":\n\t\t\tif event.PushData != nil {\n\t\t\t\tcommits += event.PushData.CommitCount\n\t\t\t}\n\t\tcase event.ActionName == \"accepted\" && event.TargetType != nil && *event.TargetType == \"MergeRequest\":\n\t\t\tmrs++\n\t\tcase event.ActionName == \"opened\" && event.TargetType != nil && *event.TargetType == \"MergeRequest\":\n\t\t\tmrs++\n\t\tdefault:\n\t\t\t// TODO: define a 'miscellaneous' category for other types of events\n\t\t\tcommits++\n\t\t}\n\t}\n\n\treturn commits, mrs, totalPages, nil\n}\n\nfunc printResults(stats []ContributionStats) {\n\n\t// loop over stats to get the average number of commits per merge request\n\tvar totalMRs, totalCommits int\n\tfor _, stat := range stats {\n\t\ttotalMRs += stat.YearlyMRs\n\t\ttotalCommits += stat.YearlyCommits\n\t}\n\n\taverageCommitsPerMR := int(float64(totalCommits) / float64(totalMRs))\n\n\t// Find maximum values for normalization\n\tvar maxDaily, maxWeekly, maxMonthly, maxYearly int\n\tfor _, stat := range stats {\n\t\tmaxDaily = max(maxDaily, stat.DailyCommits+(stat.DailyMRs*averageCommitsPerMR))\n\t\tmaxWeekly = max(maxWeekly, stat.WeeklyCommits+(stat.WeeklyMRs*averageCommitsPerMR))\n\t\tmaxMonthly = max(maxMonthly, stat.MonthlyCommits+(stat.MonthlyMRs*averageCommitsPerMR))\n\t\tmaxYearly = max(maxYearly, stat.YearlyCommits+(stat.YearlyMRs*averageCommitsPerMR))\n\t}\n\n\tvar maxDailyCommits, maxWeeklyCommits, maxMonthlyCommits, maxYearlyCommits int\n\tfor _, stat := range stats {\n\t\tmaxDailyCommits = max(maxDailyCommits, stat.DailyCommits)\n\t\tmaxWeeklyCommits = max(maxWeeklyCommits, stat.WeeklyCommits)\n\t\tmaxMonthlyCommits = max(maxMonthlyCommits, stat.MonthlyCommits)\n\t\tmaxYearlyCommits = max(maxYearlyCommits, stat.YearlyCommits)\n\t}\n\n\tvar maxDailyMRs, maxWeeklyMRs, maxMonthlyMRs, maxYearlyMRs int\n\tfor _, stat := range stats {\n\t\tmaxDailyMRs = max(maxDailyMRs, stat.DailyMRs)\n\t\tmaxWeeklyMRs = max(maxWeeklyMRs, stat.WeeklyMRs)\n\t\tmaxMonthlyMRs = max(maxMonthlyMRs, stat.MonthlyMRs)\n\t\tmaxYearlyMRs = max(maxYearlyMRs, stat.YearlyMRs)\n\t}\n\n\t// sort stats by scaled contributions (commts + scaled MRs)\n\tsort.Slice(stats, func(i, j int) bool {\n\t\treturn (stats[i].YearlyCommits + stats[i].YearlyMRs*averageCommitsPerMR) > (stats[j].YearlyCommits + stats[j].YearlyMRs*averageCommitsPerMR)\n\t})\n\n\tfmt.Println(\"\\n🏆 Top Contributors Overall\\n\")\n\tfmt.Printf(\"(MRs scaled by %dx, the global average commits per MR across Solve)\\n\", averageCommitsPerMR)\n\tfmt.Println(\"========================\")\n\n\temojisByRank := []string{\"🥇\", \"🥈\", \"🥉\"}\n\n\tfor index, stat := range stats {\n\t\t// Use the index to get the corresponding emoji - if we are less than three, use the list\n\t\temoji := \"👨‍💻\"\n\t\tif index < len(emojisByRank) {\n\t\t\temoji = emojisByRank[index]\n\t\t}\n\t\tfmt.Printf(\"\\n%s %s\\n\", emoji, stat.Username)\n\t\tfmt.Printf(\" (Fetched %d pages from GitLab)\\n\", stat.PagesFetched)\n\n\t\t// Print daily, weekly, monthly, yearly bars\n\t\tprintBar(\"Today \", stat.DailyCommits+(stat.DailyMRs*averageCommitsPerMR), maxDaily)\n\t\tprintBar(\"Week \", stat.WeeklyCommits+(stat.WeeklyMRs*averageCommitsPerMR), maxWeekly)\n\t\tprintBar(\"Month \", stat.MonthlyCommits+(stat.MonthlyMRs*averageCommitsPerMR), maxMonthly)\n\t\tprintBar(\"Year \", stat.YearlyCommits+(stat.YearlyMRs*averageCommitsPerMR), maxYearly)\n\n\t\tfmt.Printf(\"Details: %d commits, %d MRs in the past year. Total Score: %d\\n\",\n\t\t\tstat.YearlyCommits, stat.YearlyMRs, stat.YearlyCommits+(stat.YearlyMRs*averageCommitsPerMR))\n\t}\n\n\t// sort stats by commits now\n\tsort.Slice(stats, func(i, j int) bool {\n\t\treturn stats[i].YearlyCommits > stats[j].YearlyCommits\n\t})\n\n\tfmt.Println(\"\\n🏆 Top Committers\")\n\tfmt.Println(\"========================\")\n\n\tfor index, stat := range stats {\n\t\t// Use the index to get the corresponding emoji - if we are less than three, use the list\n\t\temoji := \"👨‍💻\"\n\t\tif index < len(emojisByRank) {\n\t\t\temoji = emojisByRank[index]\n\t\t}\n\t\tfmt.Printf(\"\\n%s %s\\n\", emoji, stat.Username)\n\t\tfmt.Printf(\" (Fetched %d pages from GitLab)\\n\", stat.PagesFetched)\n\n\t\t// Print daily, weekly, monthly, yearly bars\n\t\tprintBar(\"Today \", stat.DailyCommits, maxDailyCommits)\n\t\tprintBar(\"Week \", stat.WeeklyCommits, maxWeeklyCommits)\n\t\tprintBar(\"Month \", stat.MonthlyCommits, maxMonthlyCommits)\n\t\tprintBar(\"Year \", stat.YearlyCommits, maxYearlyCommits)\n\n\t\tfmt.Printf(\"Details: %d commits in the past year\\n\",\n\t\t\tstat.YearlyCommits)\n\t}\n\n\t// sort stats by MRs now\n\tsort.Slice(stats, func(i, j int) bool {\n\t\treturn stats[i].YearlyMRs > stats[j].YearlyMRs\n\t})\n\n\tfmt.Println(\"\\n🏆 Top Merge Requesters\")\n\tfmt.Println(\"========================\")\n\n\tfor index, stat := range stats {\n\t\t// Use the index to get the corresponding emoji - if we are less than three, use the list\n\t\temoji := \"👨‍💻\"\n\t\tif index < len(emojisByRank) {\n\t\t\temoji = emojisByRank[index]\n\t\t}\n\t\tfmt.Printf(\"\\n%s %s\\n\", emoji, stat.Username)\n\t\tfmt.Printf(\" (Fetched %d pages from GitLab)\\n\", stat.PagesFetched)\n\n\t\t// Print daily, weekly, monthly, yearly bars\n\t\tprintBar(\"Today \", stat.DailyMRs, maxDailyMRs)\n\t\tprintBar(\"Week \", stat.WeeklyMRs, maxWeeklyMRs)\n\t\tprintBar(\"Month \", stat.MonthlyMRs, maxMonthlyMRs)\n\t\tprintBar(\"Year \", stat.YearlyMRs, maxYearlyMRs)\n\n\t\tfmt.Printf(\"Details: %d MRs in the past year\\n\",\n\t\t\tstat.YearlyMRs)\n\t}\n}\n\nfunc printBar(label string, value, maxValue int) {\n\tconst barWidth = 30\n\tvar percentage float64\n\tif maxValue > 0 {\n\t\tpercentage = float64(value) / float64(maxValue) * 100\n\t}\n\n\tfilledWidth := int(float64(barWidth) * percentage / 100)\n\tbar := strings.Repeat(\"█\", filledWidth) + strings.Repeat(\"░\", barWidth-filledWidth)\n\n\tfmt.Printf(\"%s [%s] %.1f%% (%d)\\n\", label, bar, percentage, value)\n}\n\nfunc max(a, b int) int {\n\tif a > b {\n\t\treturn a\n\t}\n\treturn b\n}\n",
1797
+ "language": "Go",
1798
+ "caretPosition": {
1799
+ "row": 0,
1800
+ "col": 0
1801
+ }
1802
+ },
1803
+ "main_test.go": {
1804
+ "type": "file",
1805
+ "content": "package main\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/joho/godotenv\"\n\t\"solve.ch/solvers/http_helper\"\n)\n\nfunc TestGetUserActivity(t *testing.T) {\n\t// Load .env file\n\terr := godotenv.Load()\n\tif err != nil {\n\t\tlog.Println(\"Warning: Error loading .env file:\", err)\n\t}\n\n\t// Get GitLab token from environment\n\ttoken := os.Getenv(\"GITLAB_TOKEN\")\n\tif token == \"\" {\n\t\tt.Fatal(\"GITLAB_TOKEN environment variable not set\")\n\t}\n\n\t// Set up time range for last 7 days\n\tnow := time.Now()\n\tsevenDaysAgo := now.AddDate(0, 0, -7)\n\n\t// Test GetUserActivity function\n\tcommits, mrs, err := GetUserActivity(token, \"cfrewin\", sevenDaysAgo, now)\n\tif err != nil {\n\t\tt.Fatalf(\"GetUserActivity failed: %v\", err)\n\t}\n\n\t// Log the results\n\tt.Logf(\"Activity for user 'cfrewin' in the last 7 days:\")\n\tt.Logf(\"Commits: %d\", commits)\n\tt.Logf(\"Merge Requests: %d\", mrs)\n\n\t// From the sample JSON, we can see several commits and MRs\n\t// Let's add some basic assertions\n\tif commits == 0 {\n\t\tt.Log(\"Warning: No commits found in the last 7 days\")\n\t}\n\tif mrs < 0 {\n\t\tt.Error(\"Number of merge requests should not be negative\")\n\t}\n\n\t// Add detailed logging of commits by looking at the raw response\n\tresponseData, err := http_helper.MakeHTTPRequest(\n\t\t\"https://git.solve.ch/api/v4/users/cfrewin/events\",\n\t\t\"GET\",\n\t\tmap[string]string{\"PRIVATE-TOKEN\": token},\n\t\tnil,\n\t\tnil,\n\t)\n\tif err != nil {\n\t\tt.Logf(\"Failed to get raw events: %v\", err)\n\t} else {\n\t\tt.Logf(\"Raw response: %s\", string(responseData))\n\t}\n}\n",
1806
+ "language": "Go",
1807
+ "caretPosition": {
1808
+ "row": 0,
1809
+ "col": 0
1810
+ }
1811
+ },
1812
+ "usernames.txt": {
1813
+ "type": "file",
1814
+ "content": "cfrewin\nsgacond\nthuber\nsgulas\nvkamberi\nurust\nsreber\nsrupp\nmsobotka\ndavid.ritter\nrlauber\nmkohler\nakati\nmhemmi\njheron",
1815
+ "language": "Plain Text",
1816
+ "caretPosition": {
1817
+ "row": 0,
1818
+ "col": 0
1819
+ }
1820
+ }
1821
+ }
1822
+ },
1823
+ "editorSnapshot": {
1824
+ "isEditorContextMenuOpen": false,
1825
+ "editors": []
1826
+ },
1827
+ "terminalSnapshot": {
1828
+ "terminals": []
1829
+ },
1830
+ "mouseSnapshot": {
1831
+ "location": "editor",
1832
+ "currentHoveredFileName": "",
1833
+ "currentHoveredFolderName": "",
1834
+ "currentHoveredEditorTabFileName": "",
1835
+ "x": 0,
1836
+ "y": 0,
1837
+ "timestamp": 0,
1838
+ "type": "move",
1839
+ "buttonStates": {
1840
+ "left": false,
1841
+ "right": false,
1842
+ "middle": false
1843
+ },
1844
+ "scrollPosition": {
1845
+ "x": 0,
1846
+ "y": 0
1847
+ }
1848
+ },
1849
+ "authorSnapshot": {
1850
+ "authors": [
1851
+ {
1852
+ "currentSpeechCaption": ""
1853
+ }
1854
+ ]
1855
+ }
1078
1856
  }
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
- };
1857
+ }
1858
+ }
1859
+ ];
1860
+ // returns a JSON representation of an example lesson based on keywords
1861
+ // the LLM can use this to learn real example lessons created by humans
1862
+ const getExampleLessonByKeyword = (keyword) => {
1863
+ const exampleJSONs = [];
1864
+ if (Array.isArray(keyword)) {
1865
+ // if the keyword is an array, loop over all keywords and get the example lesson for each one
1866
+ for (var i = 0; i < keyword.length; i++) {
1867
+ const exampleJSON = getExampleLessonByKeyword(keyword[i]);
1868
+ exampleJSONs.push(exampleJSON);
1869
+ }
1870
+ return exampleJSONs.join(", ");
1871
+ }
1872
+ else {
1873
+ // if the keyword is a string, get the example lesson for that keyword
1874
+ const exampleJSON = getSingleExampleLesson(keyword);
1875
+ if (exampleJSON) {
1876
+ return exampleJSON;
1877
+ }
1878
+ }
1879
+ // if no keywords match, return an empty string
1880
+ return "";
1881
+ };
1882
+ const getSingleExampleLesson = (keyword) => {
1883
+ // loop over all keywords and find the first one that matches
1884
+ for (var i = 0; i < keywordsToExampleLessonsMap.length; i++) {
1885
+ const keywordMap = keywordsToExampleLessonsMap[i];
1886
+ if (keywordMap.keywords.includes(keyword)) {
1887
+ return JSON.stringify(keywordMap.lesson, null, 2);
1888
+ }
1889
+ }
1890
+ // if no keywords match, return an empty string
1891
+ return "";
1892
+ };
1893
+
1894
+ // Load environment variables from .env file
1895
+ dotenv.config();
1896
+ const ActionSchema = z.object({
1897
+ name: z.string(),
1898
+ value: z.string()
1143
1899
  });
1144
- server.tool("codevideo_create_course_with_initial_metadata", {
1900
+ const CourseSnapshotSchema = z.object({});
1901
+ const LessonSchema = z.object({
1145
1902
  id: z.string(),
1146
1903
  name: z.string(),
1147
1904
  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
- };
1905
+ actions: z.array(ActionSchema),
1906
+ initialSnapshot: CourseSnapshotSchema,
1907
+ finalSnapshot: CourseSnapshotSchema
1183
1908
  });
1184
- server.tool("codevideo_add_lesson_to_course", {
1185
- course: CourseSchema,
1909
+ const CourseSchema = z.object({
1186
1910
  id: z.string(),
1187
1911
  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
- };
1912
+ description: z.string(),
1913
+ primaryLanguage: z.string(),
1914
+ lessons: z.array(LessonSchema)
1198
1915
  });
1199
- server.tool("codevideo_get_action_names", {}, async () => {
1200
- return {
1201
- content: [{
1202
- type: "text",
1916
+ function createMcpServer() {
1917
+ const server = new McpServer({
1918
+ name: "codevideo-mcp",
1919
+ version: version,
1920
+ env: {
1921
+ // preserve existing environment variables
1922
+ ...Object.fromEntries(Object.entries(process.env)
1923
+ .filter(([, value]) => value !== undefined)
1924
+ .map(([key, value]) => [key, value])),
1925
+ MCP_TIMEOUT: String(1200000), // 20 min - needed for video creation waiting
1926
+ }
1927
+ });
1928
+ setupMcpServerTools(server);
1929
+ return server;
1930
+ }
1931
+ function setupMcpServerTools(server) {
1932
+ // Commenting out prompts and resources that aren't showing up
1933
+ // - does Claude just use them internally?
1934
+ /*
1935
+ // add prompts
1936
+ server.prompt(
1937
+ "codevideo_create_lesson",
1938
+ { additionalContext: z.string() },
1939
+ ({ additionalContext }) => ({
1940
+ messages: [{
1941
+ role: "user",
1942
+ content: {
1943
+ type: "text",
1944
+ text: createLesson(additionalContext)
1945
+ }
1946
+ }]
1947
+ })
1948
+ );
1949
+
1950
+ // add resources
1951
+ server.resource(
1952
+ "codevideo_get_action_names",
1953
+ "codevideo_get_action_names://codevideo_get_action_names",
1954
+ (uri) => ({
1955
+ contents: [{
1956
+ uri: uri.href,
1203
1957
  text: getActionNames()
1204
1958
  }]
1205
- };
1206
- });
1207
- server.tool("codevideo_get_example_lesson", {
1208
- keyword: z.string()
1209
- }, async ({ keyword }) => {
1210
- return {
1211
- content: [{
1212
- type: "text",
1959
+ })
1960
+ );
1961
+
1962
+ server.resource(
1963
+ "codevideo_get_example_lesson",
1964
+ new ResourceTemplate("codevideo_get_example_lesson://{keyword}", { list: undefined }),
1965
+ async (uri, { keyword }) => ({
1966
+ contents: [{
1967
+ uri: uri.href,
1213
1968
  text: getExampleLesson(keyword)
1214
1969
  }]
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);
1970
+ })
1971
+ );
1972
+ */
1973
+ server.tool("codevideo_instructions_to_create_a_course", {
1974
+ additionalContext: z.string()
1975
+ }, async ({ additionalContext }) => {
1258
1976
  return {
1259
1977
  content: [{
1260
1978
  type: "text",
1261
- text: JSON.stringify({
1262
- outputPath: result.outputPath,
1263
- stdout: result.stdout,
1264
- stderr: result.stderr
1265
- }, null, 2)
1979
+ text: instructionsToCreateACourse(additionalContext)
1266
1980
  }]
1267
1981
  };
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) {
1982
+ });
1983
+ server.tool("codevideo_instructions_to_create_lesson_actions", {
1984
+ additionalContext: z.string()
1985
+ }, async ({ additionalContext }) => {
1986
+ return {
1987
+ content: [{
1988
+ type: "text",
1989
+ text: instructionsToCreateLessonActions(additionalContext)
1990
+ }]
1991
+ };
1992
+ });
1993
+ server.tool("codevideo_instructions_to_create_lesson_video", {
1994
+ additionalContext: z.string()
1995
+ }, async ({ additionalContext }) => {
1996
+ return {
1997
+ content: [{
1998
+ type: "text",
1999
+ text: instructionsToCreateLessonVideo(additionalContext)
2000
+ }]
2001
+ };
2002
+ });
2003
+ server.tool("codevideo_instructions_to_translate_actions", "Generate translation instructions for CodeVideo actions. Pass actions array OR use previously stored actions from codevideo_set_current_actions (recommended workflow).", {
2004
+ actionsToTranslation: z.array(ActionSchema).optional().describe("Array of CodeVideo actions to translate. Optional if actions were previously stored with codevideo_set_current_actions"),
2005
+ targetLanguage: z.string().describe("Target language for translation (e.g., 'Spanish', 'French', 'German')")
2006
+ }, async ({ actionsToTranslation, targetLanguage }) => {
2007
+ // Cast the actions to IAction if provided
2008
+ const typedActions = actionsToTranslation;
2009
+ return {
2010
+ content: [{
2011
+ type: "text",
2012
+ text: instructionsToTranslateActions(typedActions, targetLanguage)
2013
+ }]
2014
+ };
2015
+ });
2016
+ server.tool("codevideo_get_empty_lesson_snapshot", {}, async () => {
2017
+ return {
2018
+ content: [{
2019
+ type: "text",
2020
+ text: getEmptyLessonSnapshot()
2021
+ }]
2022
+ };
2023
+ });
2024
+ server.tool("codevideo_create_course_with_initial_metadata", {
2025
+ id: z.string(),
2026
+ name: z.string(),
2027
+ description: z.string(),
2028
+ primaryLanguage: z.string()
2029
+ }, async ({ id, name, description, primaryLanguage }) => {
2030
+ return {
2031
+ content: [{
2032
+ type: "text",
2033
+ text: createCourseWithInitialMetadata(id, name, description, primaryLanguage)
2034
+ }]
2035
+ };
2036
+ });
2037
+ server.tool("codevideo_add_actions_to_lesson", {
2038
+ lesson: LessonSchema,
2039
+ actions: z.array(ActionSchema)
2040
+ }, async ({ lesson, actions }) => {
2041
+ // Cast the lesson to ILesson
2042
+ const typedLesson = lesson;
2043
+ // Cast the actions to IAction
2044
+ const typedActions = actions;
2045
+ return {
2046
+ content: [{
2047
+ type: "text",
2048
+ text: addActionsToLesson(typedLesson, typedActions)
2049
+ }]
2050
+ };
2051
+ });
2052
+ 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).", {
2053
+ actions: z.array(ActionSchema).optional().describe("Array of CodeVideo actions to apply. Optional if actions were previously stored with codevideo_set_current_actions")
2054
+ }, async ({ actions }) => {
2055
+ // Cast the actions to IAction if provided
2056
+ const typedActions = actions;
2057
+ return {
2058
+ content: [{
2059
+ type: "text",
2060
+ text: getFinalLessonSnapshot(typedActions)
2061
+ }]
2062
+ };
2063
+ });
2064
+ server.tool("codevideo_add_lesson_to_course", {
2065
+ course: CourseSchema,
2066
+ id: z.string(),
2067
+ name: z.string(),
2068
+ description: z.string()
2069
+ }, async ({ course, id, name, description }) => {
2070
+ // Cast the course to ICourse
2071
+ const typedCourse = course;
2072
+ return {
2073
+ content: [{
2074
+ type: "text",
2075
+ text: addLessonToCourse(typedCourse, id, name, description)
2076
+ }]
2077
+ };
2078
+ });
2079
+ server.tool("codevideo_get_action_names", {}, async () => {
2080
+ return {
2081
+ content: [{
2082
+ type: "text",
2083
+ text: getActionNames()
2084
+ }]
2085
+ };
2086
+ });
2087
+ server.tool("codevideo_get_example_actions_array_by_keyword", {
2088
+ keyword: z.string()
2089
+ }, async ({ keyword }) => {
2090
+ return {
2091
+ content: [{
2092
+ type: "text",
2093
+ text: getExampleActionsArrayByKeyword(keyword)
2094
+ }]
2095
+ };
2096
+ });
2097
+ server.tool("codevideo_get_example_lesson_by_keyword", {
2098
+ keyword: z.string()
2099
+ }, async ({ keyword }) => {
2100
+ return {
2101
+ content: [{
2102
+ type: "text",
2103
+ text: getExampleLessonByKeyword(keyword)
2104
+ }]
2105
+ };
2106
+ });
2107
+ server.tool("codevideo_get_initial_lesson_state_from_local_repo", "Create an initial lesson state snapshot from a local repository/codebase directory. This captures the file structure and creates an empty lesson template ready for action recording.", {
2108
+ repoLocation: z.string().describe("Local file system path to the repository/codebase directory")
2109
+ }, async ({ repoLocation }) => {
2110
+ return {
2111
+ content: [{
2112
+ type: "text",
2113
+ text: await getInitialLessonStateFromLocalCodebase(repoLocation)
2114
+ }]
2115
+ };
2116
+ });
2117
+ server.tool("codevideo_get_initial_lesson_state_from_github_repo", "Create an initial lesson state snapshot from a GitHub repository URL. This fetches the repository content, captures the file structure, and creates an empty lesson template ready for action recording.", {
2118
+ repoUrl: z.string().describe("GitHub repository URL (e.g., https://github.com/user/repo)")
2119
+ }, async ({ repoUrl }) => {
2120
+ return {
2121
+ content: [{
2122
+ type: "text",
2123
+ text: await getInitialLessonStateFromGitHubRepo(repoUrl)
2124
+ }]
2125
+ };
2126
+ });
2127
+ 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).", {
2128
+ actions: z.array(ActionSchema).optional().describe("Array of CodeVideo actions. Optional if actions were previously stored with codevideo_set_current_actions")
2129
+ }, async ({ actions }) => {
2130
+ // Cast the actions to IAction if provided
2131
+ const typedActions = actions;
2132
+ return {
2133
+ content: [{
2134
+ type: "text",
2135
+ text: makeBlogPost(typedActions)
2136
+ }]
2137
+ };
2138
+ });
2139
+ 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).", {
2140
+ actions: z.array(ActionSchema).optional().describe("Array of CodeVideo actions. Optional if actions were previously stored with codevideo_set_current_actions")
2141
+ }, async ({ actions }) => {
2142
+ // Cast the actions to IAction if provided
2143
+ const typedActions = actions;
2144
+ return {
2145
+ content: [{
2146
+ type: "text",
2147
+ text: await makeHTMLWebPage(typedActions)
2148
+ }]
2149
+ };
2150
+ });
2151
+ server.tool("codevideo_make_video_from_actions", "Generate a video from CodeVideo actions. Pass actions array OR use previously stored actions from codevideo_set_current_actions (recommended workflow).", {
2152
+ actions: z.array(ActionSchema).optional().describe("Array of CodeVideo actions. Optional if actions were previously stored with codevideo_set_current_actions")
2153
+ }, async ({ actions }) => {
2154
+ // Cast the actions to IAction if provided
2155
+ const typedActions = actions;
2156
+ try {
2157
+ const result = await makeVideoFromActions(typedActions);
1273
2158
  return {
1274
2159
  content: [{
1275
2160
  type: "text",
1276
- text: `Error creating video: ${error.error?.message || String(error.error)}\nstdout: ${error.stdout}\nstderr: ${error.stderr}`
2161
+ text: JSON.stringify({
2162
+ outputPath: result.outputPath,
2163
+ stdout: result.stdout,
2164
+ stderr: result.stderr
2165
+ }, null, 2)
1277
2166
  }]
1278
2167
  };
1279
2168
  }
1280
- // Fall back to simple error message
2169
+ catch (error) {
2170
+ console.error('Error making video:', error);
2171
+ // If the error has the VideoResult structure
2172
+ if (error && typeof error === 'object' && 'stdout' in error && 'stderr' in error) {
2173
+ return {
2174
+ content: [{
2175
+ type: "text",
2176
+ text: `Error creating video: ${error.error?.message || String(error.error)}\nstdout: ${error.stdout}\nstderr: ${error.stderr}`
2177
+ }]
2178
+ };
2179
+ }
2180
+ // Fall back to simple error message
2181
+ return {
2182
+ content: [{
2183
+ type: "text",
2184
+ text: `Error creating video: ${error?.message || String(error)}`
2185
+ }]
2186
+ };
2187
+ }
2188
+ });
2189
+ server.tool("codevideo_make_video_from_lesson", "Generate a video from a complete lesson object. Pass a lesson object OR use previously stored lesson from lesson management tools. This is useful when you have a full lesson with snapshots rather than just actions.", {
2190
+ lesson: LessonSchema.optional().describe("Complete lesson object with snapshots. Optional if lesson was previously stored with lesson management tools")
2191
+ }, async ({ lesson }) => {
2192
+ // Cast the lesson to ILesson if provided
2193
+ const typedLesson = lesson;
2194
+ try {
2195
+ const result = await makeVideoFromLesson(typedLesson);
2196
+ return {
2197
+ content: [{
2198
+ type: "text",
2199
+ text: JSON.stringify({
2200
+ outputPath: result.outputPath,
2201
+ stdout: result.stdout,
2202
+ stderr: result.stderr
2203
+ }, null, 2)
2204
+ }]
2205
+ };
2206
+ }
2207
+ catch (error) {
2208
+ console.error('Error making video from lesson:', error);
2209
+ // If the error has the VideoResult structure
2210
+ if (error && typeof error === 'object' && 'stdout' in error && 'stderr' in error) {
2211
+ return {
2212
+ content: [{
2213
+ type: "text",
2214
+ text: `Error creating video from lesson: ${error.error?.message || String(error.error)}\nstdout: ${error.stdout}\nstderr: ${error.stderr}`
2215
+ }]
2216
+ };
2217
+ }
2218
+ // Fall back to simple error message
2219
+ return {
2220
+ content: [{
2221
+ type: "text",
2222
+ text: `Error creating video from lesson: ${error?.message || String(error)}`
2223
+ }]
2224
+ };
2225
+ }
2226
+ });
2227
+ server.tool("codevideo_validate_actions", {
2228
+ actions: z.array(ActionSchema).optional()
2229
+ }, async ({ actions }) => {
2230
+ // Cast the actions to IAction if provided
2231
+ const typedActions = actions;
1281
2232
  return {
1282
2233
  content: [{
1283
2234
  type: "text",
1284
- text: `Error creating video: ${error?.message || String(error)}`
2235
+ text: validateActions(typedActions)
1285
2236
  }]
1286
2237
  };
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);
2238
+ });
2239
+ server.tool("codevideo_is_repeatable_action", {
2240
+ action: ActionSchema
2241
+ }, async ({ action }) => {
2242
+ // Cast the action to IAction
2243
+ const typedAction = action;
1321
2244
  return {
1322
2245
  content: [{
1323
2246
  type: "text",
1324
- text: critique
2247
+ text: isRepeatable(typedAction)
1325
2248
  }]
1326
2249
  };
1327
- }
1328
- catch (error) {
1329
- const errorMessage = error instanceof Error ? error.message : String(error);
1330
- console.error("Error critiquing video:", errorMessage);
2250
+ });
2251
+ 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).", {
2252
+ videoPath: z.string().describe("Path to the video file to critique"),
2253
+ 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")
2254
+ }, async ({ videoPath, actions }) => {
2255
+ // Cast the actions to IAction if provided
2256
+ const typedActions = actions;
2257
+ try {
2258
+ const critique = await critiqueVideoUsingGemini(videoPath, typedActions);
2259
+ return {
2260
+ content: [{
2261
+ type: "text",
2262
+ text: critique
2263
+ }]
2264
+ };
2265
+ }
2266
+ catch (error) {
2267
+ const errorMessage = error instanceof Error ? error.message : String(error);
2268
+ console.error("Error critiquing video:", errorMessage);
2269
+ return {
2270
+ content: [{
2271
+ type: "text",
2272
+ text: `Error critiquing video: ${errorMessage}`
2273
+ }]
2274
+ };
2275
+ }
2276
+ });
2277
+ 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.", {
2278
+ actions: z.array(ActionSchema).describe("Array of CodeVideo actions to store for later use")
2279
+ }, async ({ actions }) => {
2280
+ // Cast the actions to IAction
2281
+ const typedActions = actions;
1331
2282
  return {
1332
2283
  content: [{
1333
2284
  type: "text",
1334
- text: `Error critiquing video: ${errorMessage}`
2285
+ text: setCurrentActions(typedActions)
1335
2286
  }]
1336
2287
  };
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) {
2288
+ });
2289
+ 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 () => {
2290
+ const actions = getCurrentActions();
2291
+ if (!actions) {
2292
+ return {
2293
+ content: [{
2294
+ type: "text",
2295
+ text: "No actions found in storage. Use codevideo_set_current_actions first to store actions."
2296
+ }]
2297
+ };
2298
+ }
1354
2299
  return {
1355
2300
  content: [{
1356
2301
  type: "text",
1357
- text: "No actions found in storage. Use codevideo_set_current_actions first to store actions."
2302
+ text: JSON.stringify(actions, null, 2)
1358
2303
  }]
1359
2304
  };
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
- })();
2305
+ });
2306
+ server.tool("codevideo_set_current_lesson", "Store a complete lesson object in local SQLite database for reuse across multiple tool calls. Once set, other tools can be called without re-specifying the lesson.", {
2307
+ lesson: LessonSchema.describe("Complete lesson object to store for later use")
2308
+ }, async ({ lesson }) => {
2309
+ // Cast the lesson to ILesson
2310
+ const typedLesson = lesson;
2311
+ return {
2312
+ content: [{
2313
+ type: "text",
2314
+ text: setCurrentLesson(typedLesson)
2315
+ }]
2316
+ };
2317
+ });
2318
+ server.tool("codevideo_get_current_lesson", "Retrieve the currently stored lesson from local database. Use this to inspect what lesson was previously set with codevideo_set_current_lesson.", {}, async () => {
2319
+ const lesson = getCurrentLesson();
2320
+ if (!lesson) {
2321
+ return {
2322
+ content: [{
2323
+ type: "text",
2324
+ text: "No lesson found in storage. Use codevideo_set_current_lesson first to store a lesson."
2325
+ }]
2326
+ };
2327
+ }
2328
+ return {
2329
+ content: [{
2330
+ type: "text",
2331
+ text: JSON.stringify(lesson, null, 2)
2332
+ }]
2333
+ };
2334
+ });
2335
+ }
2336
+ // start the server when running directly
2337
+ if (import.meta.url === `file://${process.argv[1]}`) {
2338
+ const server = createMcpServer();
2339
+ const transport = new StdioServerTransport();
2340
+ (async () => {
2341
+ await server.connect(transport);
2342
+ })();
2343
+ }
2344
+
2345
+ export { createMcpServer };