@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/README.md +5 -1
- package/dist/index.js +1327 -354
- package/dist/src/constants/Constants.d.ts +2 -0
- package/dist/src/index.d.ts +2 -1
- package/dist/src/prompts/instructions_to_translate_actions.d.ts +2 -0
- package/dist/src/resources/get_example_actions_array_by_keyword.d.ts +1 -0
- package/dist/src/resources/get_example_lessons_by_keyword.d.ts +1 -0
- package/dist/src/rest-server/index.d.ts +24 -0
- package/dist/src/rest-server/job-manager.d.ts +56 -0
- package/dist/src/rest-server/mcp-client.d.ts +23 -0
- package/dist/src/rest-server/rest-server.d.ts +24 -0
- package/dist/src/rest-server/server.d.ts +4 -0
- package/dist/src/tools/get_current_lesson.d.ts +3 -0
- package/dist/src/tools/get_initial_lesson_state_from_github_repo.d.ts +1 -0
- package/dist/src/tools/{make_video.d.ts → make_video_from_actions.d.ts} +1 -1
- package/dist/src/tools/make_video_from_lesson.d.ts +8 -0
- package/dist/src/tools/set_current_lesson.d.ts +2 -0
- package/package.json +19 -5
- package/dist/src/resources/get_example_lesson.d.ts +0 -1
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
|
|
9
|
-
import os
|
|
10
|
-
import fs
|
|
11
|
-
import {
|
|
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.
|
|
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
|
|
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
|
-
|
|
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\`, \`
|
|
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.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
if (!
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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(
|
|
625
|
-
|
|
626
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
1521
|
+
const getSingleExampleActionsArray = (keyword) => {
|
|
1021
1522
|
// loop over all keywords and find the first one that matches
|
|
1022
|
-
for (var i = 0; i <
|
|
1023
|
-
const keywordMap =
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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
|
-
//
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
)
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
-
|
|
1185
|
-
course: CourseSchema,
|
|
1909
|
+
const CourseSchema = z.object({
|
|
1186
1910
|
id: z.string(),
|
|
1187
1911
|
name: z.string(),
|
|
1188
|
-
description: z.string()
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
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
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
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:
|
|
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
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
2235
|
+
text: validateActions(typedActions)
|
|
1285
2236
|
}]
|
|
1286
2237
|
};
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
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:
|
|
2247
|
+
text: isRepeatable(typedAction)
|
|
1325
2248
|
}]
|
|
1326
2249
|
};
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
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:
|
|
2285
|
+
text: setCurrentActions(typedActions)
|
|
1335
2286
|
}]
|
|
1336
2287
|
};
|
|
1337
|
-
}
|
|
1338
|
-
})
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
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:
|
|
2302
|
+
text: JSON.stringify(actions, null, 2)
|
|
1358
2303
|
}]
|
|
1359
2304
|
};
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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 };
|