@fullstackcraftllc/codevideo-mcp 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +101 -4
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.js +3547 -1139
  4. package/dist/index.js.map +7 -0
  5. package/package.json +49 -28
  6. package/bin/darwin-arm64/codevideo-cli +0 -0
  7. package/bin/darwin-x64/codevideo-cli +0 -0
  8. package/bin/linux-x64/codevideo-cli +0 -0
  9. package/bin/win32-x64/codevideo-cli.exe +0 -0
  10. package/dist/src/index.d.ts +0 -1
  11. package/dist/src/prompts/instructions_to_create_a_course.d.ts +0 -1
  12. package/dist/src/prompts/instructions_to_create_lesson_actions.d.ts +0 -1
  13. package/dist/src/prompts/instructions_to_create_lesson_video.d.ts +0 -1
  14. package/dist/src/resources/get_action_names.d.ts +0 -4
  15. package/dist/src/resources/get_example_lesson.d.ts +0 -1
  16. package/dist/src/tools/add_actions_to_lesson.d.ts +0 -2
  17. package/dist/src/tools/add_final_snapshot_to_lesson.d.ts +0 -2
  18. package/dist/src/tools/add_initial_snapshot_to_lesson.d.ts +0 -2
  19. package/dist/src/tools/add_lesson_to_course.d.ts +0 -2
  20. package/dist/src/tools/create_course_with_initial_metadata.d.ts +0 -1
  21. package/dist/src/tools/critique_video_using_gemini.d.ts +0 -2
  22. package/dist/src/tools/get_current_actions.d.ts +0 -3
  23. package/dist/src/tools/get_empty_lesson_snapshot.d.ts +0 -1
  24. package/dist/src/tools/get_final_lesson_snapshot.d.ts +0 -2
  25. package/dist/src/tools/get_initial_lesson_state_from_local_repo.d.ts +0 -1
  26. package/dist/src/tools/is_repeatable_action.d.ts +0 -2
  27. package/dist/src/tools/make_blog_post.d.ts +0 -2
  28. package/dist/src/tools/make_html_web_page.d.ts +0 -2
  29. package/dist/src/tools/make_video.d.ts +0 -8
  30. package/dist/src/tools/set_current_actions.d.ts +0 -2
  31. package/dist/src/tools/validate_actions.d.ts +0 -6
  32. package/dist/src/utils/getBinaryPath.d.ts +0 -1
package/dist/index.js CHANGED
@@ -1,23 +1,22 @@
1
- import dotenv from 'dotenv';
2
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
- import { z } from 'zod';
5
- import { AllActionStrings, isRepeatableAction, isValidAction } from '@fullstackcraftllc/codevideo-types';
6
- import { VirtualIDE } from '@fullstackcraftllc/codevideo-virtual-ide';
7
- import Database from 'better-sqlite3';
8
- import path, { join } from 'path';
9
- import os, { platform, arch } from 'os';
10
- import fs, { existsSync } from 'fs';
11
- import { buildFileStructureFromRootFolder } from '@fullstackcraftllc/codevideo-adapters';
12
- import { GoogleGenAI } from '@google/genai';
13
- import { generateMarkdownStringFromActions, generateHtmlStringFromActions } from '@fullstackcraftllc/codevideo-exporters';
14
- import { spawn } from 'child_process';
15
-
16
- var version = "1.0.0";
17
-
18
- // this prompt describes how to create a course lesson using the CodeVideo framework
19
- const instructionsToCreateACourse = (additionalContext) => {
20
- return `You are an expert senior software engineer and educator, and skilled in describing software concepts in a way that is easy for students to understand. You are going to create a course using the CodeVideo framework, a framework that uses simple JSON to generate software courses and lessons in a variety of formats (video, markdown, pdf, and more).
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import dotenv from "dotenv";
5
+ import fs9 from "node:fs";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ // package.json
9
+ var version = "1.1.0";
10
+
11
+ // src/index.ts
12
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
+ import { z as z2 } from "zod";
15
+ import { AllActionStrings, isRepeatableAction as isRepeatableAction2 } from "@fullstackcraftllc/codevideo-types";
16
+
17
+ // src/prompts/instructions_to_create_a_course.ts
18
+ var instructionsToCreateACourse = (additionalContext) => {
19
+ return `You are an expert senior software engineer and educator, and skilled in describing software concepts in a way that is easy for students to understand. You are going to create a course using the CodeVideo framework, a framework that uses simple JSON to generate software courses and lessons in a variety of formats (video, markdown, pdf, and more).
21
20
 
22
21
  There are a few rules to uphold when making a CodeVideo course:
23
22
 
@@ -61,7 +60,7 @@ const instructionsToCreateACourse = (additionalContext) => {
61
60
  1. Create the initial course object using the MCP tool \`codevideo_create_course_with_initial_metadata\`
62
61
  2. Plan out the number of lessons needed and what they will be about
63
62
  3. For each lesson, create a unique identifier, name, and description
64
- 4. Create the initial empty lesson snapshot for the first lesson using the MCP tool \`codevideo_get_empty_lesson_snapshot\`. In the case of lessons beyond the first, copy the final lesson snapshot of the previous lesson and use that as the initial lesson snapshot.
63
+ 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
64
  5. Create actions for the lesson using the MCP tool \`codevideo_instructions_to_create_lesson_actions\`
66
65
  6. Add actions to the lesson using the MCP tool \`codevideo_add_actions_to_lesson\`
67
66
  7. Create the final lesson snapshot using the MCP tool \`codevideo_get_final_lesson_snapshot\`
@@ -73,9 +72,36 @@ const instructionsToCreateACourse = (additionalContext) => {
73
72
  Good luck, and enjoy creating your course!`;
74
73
  };
75
74
 
76
- // this prompt describes how to make lesson actions in the CodeVideo framework
77
- const instructionsToCreateLessonActions = (additionalContext) => {
78
- return `You are an expert senior software engineer and educator, and skilled in describing software concepts in a way that is easy for students to understand. You are going to create lesson actions using the CodeVideo framework, a framework that uses simple JSON to generate tutorials in a variety of formats (video, markdown, pdf, and more). This JSON array is always an array of simple \`IAction\`:
75
+ // src/prompts/actionReference.ts
76
+ import { AllActionDescriptions } from "@fullstackcraftllc/codevideo-types";
77
+ var actionReference = () => AllActionDescriptions.map((action) => `${action.name}: ${action.description} Value: ${action.valueDescription}`).join("\n");
78
+
79
+ // src/prompts/authoringRules.ts
80
+ var AUTHORING_RULES = `ACTION STYLE AND PACING (this is what makes lessons clear):
81
+ - This JSON array is always an array of simple IAction objects: { name: AllActions, value: string }. codevideo_get_action_names lists every action with what it does and what its value takes, so check the rows for the actions you use; codevideo_get_example_actions_array_by_keyword / codevideo_get_example_lessons_by_keyword give verified human-made examples.
82
+ - The "value" of any action is NEVER empty. Repeatable actions (e.g. editor-enter) take a numeric STRING value, usually "1"; check with codevideo_is_repeatable_action.
83
+ - Interleave speech with typing \u2014 no long silent blocks of code. Narrate the WHY before you write, and keep individual speak actions to 2-3 sentences; break longer narration into multiple author-speak-before actions.
84
+ - Mind the flow of time: speak about saving BEFORE the editor-save; speak about opening a terminal BEFORE terminal-open. REGARDLESS OF ORDER, only ever use the author-speak-before speaking action.
85
+ - Show every new file being made, one of two ways:
86
+ - The mouse (the default): mouse-move-file-explorer "1" -> mouse-right-click "1" -> mouse-move-file-explorer-context-menu-new-file "1" -> mouse-left-click "1" -> file-explorer-type-new-file-input "<filename>" -> file-explorer-enter-new-file-input "1". For a file inside a folder, start with mouse-move-file-explorer-folder "<folder>" and use mouse-move-file-explorer-folder-context-menu-new-file instead.
87
+ - The terminal, for more technical lessons: terminal-open "1" (if the terminal isn't open yet) -> terminal-type "touch <path>" -> terminal-enter "1".
88
+ Folders the same way: mouse-move-file-explorer-context-menu-new-folder with file-explorer-type-new-folder-input / file-explorer-enter-new-folder-input, or terminal-type "mkdir <path>".
89
+ - Never use file-explorer-create-file, file-explorer-create-folder or file-explorer-set-file-contents: they change files with nothing on screen causing it, so things just appear. Don't use the composite-* actions or editor-highlight-code either: they aren't supported yet and do nothing. codevideo_validate_course rejects lessons that use any of these.
90
+ - A file you just created with the mouse is ALREADY open in the editor \u2014 type into it immediately with editor-type; do NOT reopen it. A file made with touch is not open yet: open it as below. When a lesson continues from the previous one, the file you last edited is already the active editor.
91
+ - To open a DIFFERENT existing file: mouse-move-file-explorer-file "<filename>" -> file-explorer-open-file "<filename>". IMPORTANT: file-explorer-open-file's value is the FILENAME, not "1" \u2014 using "1" opens a stray editor and your typing is lost.
92
+ - Save with editor-save once a file's typing is done (narrating it first): compile checks and file reads see saved contents only.
93
+ - editor-type's value is the literal text to insert (including newlines).
94
+
95
+ NARRATION:
96
+ - Every lesson opens with an author-speak-before (codevideo_validate_course checks this). The first lesson frames what the course builds. Every lesson after the first opens with one sentence recapping the previous lesson (codevideo_add_lesson returns its name and description), e.g. "In the last lesson, we built an add function." Vary the wording from lesson to lesson. If the lesson starts something new, bridge instead: "Last time we built X; now we start Y."
97
+ - After the recap sentence you may show what it refers to: at most two stops, each an action followed by the sentence about what's now on screen. Bring an open file forward with mouse-move-editor-tab "<path>" + mouse-left-click, or open one with mouse-move-file-explorer-file "<path>" + file-explorer-open-file "<path>"; codevideo_add_lesson lists the previous lesson's files and whether they're open. Pick what this lesson builds on. Don't select code in a recap: the next editor-type would replace the selection.
98
+ - Use the inclusive "we" for what happens on screen: "Now we export the function." Switch to "you" for options, advice, caveats and things to try: "You can also write this as an arrow function." Never "I".
99
+ - End each lesson with a short summary, after its final save.
100
+ - Write file and code names as they appear on screen (math.ts, useState): captions show your exact words.`;
101
+
102
+ // src/prompts/instructions_to_create_lesson_actions.ts
103
+ var instructionsToCreateLessonActions = (additionalContext) => {
104
+ return `You are an expert senior software engineer and educator, and skilled in describing software concepts in a way that is easy for students to understand. You are going to create lesson actions using the CodeVideo framework, a framework that uses simple JSON to generate tutorials in a variety of formats (video, markdown, pdf, and more). This JSON array is always an array of simple \`IAction\`:
79
105
 
80
106
  \`\`\`typescript
81
107
  export interface IAction {
@@ -106,26 +132,53 @@ const instructionsToCreateLessonActions = (additionalContext) => {
106
132
  | ExternalActions;
107
133
  \`\`\`
108
134
 
109
- Here are all the action types you have to choose from:
135
+ Here is every action you can choose from, with what it does and what its value takes:
110
136
 
111
- \`\`\`typescript
112
- export const AllActionStrings = [
113
- ${AllActionStrings.map((action) => `"${action}"`).join(",\n ")}
114
- ];
137
+ \`\`\`
138
+ ${actionReference()}
115
139
  \`\`\`
116
140
 
117
- When in doubt with these names, feel free to use the MCP tool \`codevideo_get_action_names\` to get the most updated list of all the actions available. You can also use the MCP tool \`codevideo_get_example_lesson\` to get verified, accurate, and correct lessons created by humans.
141
+ 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
142
 
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!
143
+ ${AUTHORING_RULES}
144
+
145
+ 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_markdown_from_actions\`, etc. without needing to re-specify the actions each time. These tools will automatically retrieve the stored actions if no actions parameter is provided. Videos render as background jobs: the video tools return a jobId at once, and \`codevideo_get_render_job\` reports progress and the saved file. For a course of several lessons, use \`codevideo_instructions_to_build_course_from_scratch\` and the course tools instead of this single shared slot.
146
+
147
+ 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.
120
148
 
121
- WORKFLOW TIP: After creating actions, you can store them using \`codevideo_set_current_actions\` and then use other tools like \`codevideo_validate_actions\`, \`codevideo_make_video\`, \`codevideo_make_blog_post\`, etc. without needing to re-specify the actions each time. These tools will automatically retrieve the stored actions if no actions parameter is provided.
149
+ ACTIONS vs FULL LESSON OBJECTS:
150
+ You have two approaches for creating lessons:
151
+
152
+ 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.
153
+
154
+ 2. **FULL LESSON OBJECT APPROACH**: Use when you need to:
155
+ - Start from an existing codebase (use \`codevideo_get_initial_lesson_state_from_local_repo\` or \`codevideo_get_initial_lesson_state_from_github_repo\`)
156
+ - Continue from a previous lesson's ending state (use \`codevideo_get_final_lesson_snapshot\` from a previous lesson)
157
+ - Have complete control over the initial and final snapshots
158
+ - Use \`codevideo_make_video_from_lesson\` for this approach
122
159
 
123
160
  RECOMMENDED WORKFLOW:
124
- 1. Create your actions array
125
- 2. Call \`codevideo_set_current_actions\` once with the actions
126
- 3. Call \`codevideo_validate_actions\` WITHOUT the actions parameter - it will use the stored actions automatically - if issues are found, fix them in the actions array and call \`codevideo_set_current_actions\` again
127
- 4. Call any other tools (make_video, make_blog_post, etc.) WITHOUT the actions parameter - they will use the stored actions automatically
128
- 5. Use \`codevideo_get_current_actions\` at any later time to inspect what's currently stored if needed
161
+ 1. **DECIDE YOUR APPROACH**:
162
+ - For simple tutorials starting from scratch: Use ACTIONS-ONLY approach
163
+ - For tutorials starting from existing code or continuing from previous lessons: Use FULL LESSON OBJECT approach
164
+
165
+ 2. Call the \`codevideo_get_example_lesson\` tool to examine example lessons to get a feel for the codevideo actions and composition.
166
+ 3. Use the \`codevideo_get_action_names\` tool to get the complete and most up-to-date list of actions.
167
+
168
+ 4. **IF USING FULL LESSON OBJECT APPROACH**:
169
+ 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\`
170
+ 4b. If continuing from a previous lesson, use \`codevideo_get_final_lesson_snapshot\` from the previous lesson's actions
171
+ 4c. Create your actions array and build the complete lesson object with initial/final snapshots
172
+ 4d. Optionally store the lesson using \`codevideo_set_current_lesson\` for later retrieval
173
+ 4e. Use \`codevideo_make_video_from_lesson\` with the complete lesson object
174
+
175
+ 5. **IF USING ACTIONS-ONLY APPROACH**:
176
+ 5a. Create the actions array
177
+ 5b. Call \`codevideo_set_current_actions\` once with the actions
178
+ 5c. Call \`codevideo_validate_actions\` WITHOUT the actions parameter - it will use the stored actions automatically
179
+ 5d. Call \`codevideo_make_video_from_actions\` WITHOUT the actions parameter - it will use the stored actions automatically
180
+
181
+ 6. Use \`codevideo_get_current_actions\` or \`codevideo_get_current_lesson\` at any later time to inspect what's currently stored if needed
129
182
 
130
183
  With all this in mind, you are to create a lesson about the following:
131
184
 
@@ -135,9 +188,9 @@ const instructionsToCreateLessonActions = (additionalContext) => {
135
188
  `;
136
189
  };
137
190
 
138
- // this prompt describes how to make lesson actions in the CodeVideo framework
139
- const instructionsToCreateLessonVideo = (additionalContext) => {
140
- return `You are an expert senior software engineer and educator, and skilled in describing software concepts in a way that is easy for students to understand. You are going to create a video lesson using the CodeVideo framework, a framework that uses simple JSON to generate tutorials in a variety of formats (video, markdown, pdf, and more). The JSON array used to create video is always an array of simple type \`IAction\`:
191
+ // src/prompts/instructions_to_create_lesson_video.ts
192
+ var instructionsToCreateLessonVideo = (additionalContext) => {
193
+ return `You are an expert senior software engineer and educator, and skilled in describing software concepts in a way that is easy for students to understand. You are going to create a video lesson using the CodeVideo framework, a framework that uses simple JSON to generate tutorials in a variety of formats (video, markdown, pdf, and more). The JSON array used to create video is always an array of simple type \`IAction\`:
141
194
 
142
195
  \`\`\`typescript
143
196
  export interface IAction {
@@ -168,23 +221,46 @@ const instructionsToCreateLessonVideo = (additionalContext) => {
168
221
  | ExternalActions;
169
222
  \`\`\`
170
223
 
171
- Here are all the action types you have to choose from:
224
+ Here is every action you can choose from, with what it does and what its value takes:
172
225
 
173
- \`\`\`typescript
174
- export const AllActionStrings = [
175
- ${AllActionStrings.map((action) => `"${action}"`).join(",\n ")}
176
- ];
226
+ \`\`\`
227
+ ${actionReference()}
177
228
  \`\`\`
178
229
 
179
230
  When in doubt with these names, feel free to use the MCP tool \`codevideo_get_action_names\` to get the most updated list of all the actions available. You can also use the MCP tool \`codevideo_get_example_lesson\` to get verified, accurate, and correct lessons created by humans.
180
231
 
181
- Also VERY IMPORTANT to note, while most 'value' properties are corresponding strings, things like \`editor-enter\` are considered "Repeatable" properties, which MUST have a string number as the 'value' property, which is 99% of the time the string "1", but can be any positive integer, representing the number of times to repeat. An action can be checked for repeatability with the MCP tool \`codevideo_is_repeatable_action\`. To be stressed again: the "value" property of any codevideo action CANNOT be left empty!
232
+ ${AUTHORING_RULES}
233
+
234
+ 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.
235
+
236
+ ACTIONS vs FULL LESSON OBJECTS - CHOOSE YOUR APPROACH:
237
+ You have two approaches for creating video lessons:
238
+
239
+ 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.
240
+
241
+ 2. **FULL LESSON OBJECT APPROACH**: Use when you need to:
242
+ - Start from an existing codebase (use \`codevideo_get_initial_lesson_state_from_local_repo\` or \`codevideo_get_initial_lesson_state_from_github_repo\`)
243
+ - Continue from a previous lesson's ending state (use \`codevideo_get_final_lesson_snapshot\` from a previous lesson)
244
+ - Have complete control over the initial and final snapshots
245
+ - Use \`codevideo_make_video_from_lesson\` for this approach
182
246
 
183
247
  RECOMMENDED WORKFLOW:
184
- 1. Create your actions array
185
- 2. Call \`codevideo_set_current_actions\` once with the actions
186
- 3. Call \`codevideo_validate_actions\` WITHOUT the actions parameter - it will use the stored actions automatically - if issues are found, fix them in the actions array and call \`codevideo_set_current_actions\` again
187
- 4. Call \`codevideo_make_video\` WITHOUT the actions parameter - it will use the stored actions automatically
248
+ 1. **DECIDE YOUR APPROACH**:
249
+ - For simple tutorials starting from scratch: Use ACTIONS-ONLY approach
250
+ - For tutorials starting from existing code or continuing from previous lessons: Use FULL LESSON OBJECT approach
251
+
252
+ 2. **IF USING ACTIONS-ONLY APPROACH**:
253
+ 2a. Create your actions array
254
+ 2b. Call \`codevideo_set_current_actions\` once with the actions
255
+ 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
256
+ 2d. Call \`codevideo_make_video_from_actions\` WITHOUT the actions parameter - it will use the stored actions automatically
257
+
258
+ 3. **IF USING FULL LESSON OBJECT APPROACH**:
259
+ 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\`
260
+ 3b. If continuing from a previous lesson, use \`codevideo_get_final_lesson_snapshot\` from the previous lesson's actions
261
+ 3c. Create your actions array and build the complete lesson object with initial/final snapshots
262
+ 3d. Optionally store the lesson using \`codevideo_set_current_lesson\` for later retrieval
263
+ 3e. Use \`codevideo_make_video_from_lesson\` with the complete lesson object
188
264
 
189
265
  With all this in mind, you are to create a lesson about the following:
190
266
 
@@ -194,1179 +270,3511 @@ const instructionsToCreateLessonVideo = (additionalContext) => {
194
270
  `;
195
271
  };
196
272
 
197
- // retrieves the current actions from the key value sqlite database
198
- const getCurrentActions = () => {
273
+ // src/constants/Constants.ts
274
+ var CURRENT_ACTIONS_KEY = "current_actions";
275
+ var CURRENT_LESSON_KEY = "current_lesson";
276
+
277
+ // src/store/db.ts
278
+ import fs from "node:fs";
279
+ import os from "node:os";
280
+ import path from "node:path";
281
+ import { createRequire } from "node:module";
282
+ var codevideoHome = () => {
283
+ const dir = path.join(os.homedir(), ".codevideo");
284
+ fs.mkdirSync(dir, { recursive: true });
285
+ return dir;
286
+ };
287
+ var databasePath = () => path.join(codevideoHome(), "codevideo.db");
288
+ var sqlite;
289
+ var loadSqlite = () => {
290
+ if (!sqlite) {
291
+ const emitWarning = process.emitWarning;
292
+ process.emitWarning = ((warning, ...rest) => {
293
+ const message = typeof warning === "string" ? warning : warning.message;
294
+ if (/SQLite/.test(message)) return;
295
+ return emitWarning.call(process, warning, ...rest);
296
+ });
199
297
  try {
200
- const homeDir = os.homedir();
201
- const dbPath = path.join(homeDir, '.codevideo', 'codevideo.db');
202
- // Check if database file exists
203
- if (!fs.existsSync(dbPath)) {
204
- return null;
205
- }
206
- // Open database
207
- const db = new Database(dbPath);
208
- // Check if table exists
209
- const tableExists = db.prepare(`
210
- SELECT name FROM sqlite_master
211
- WHERE type='table' AND name='key_value_store'
212
- `).get();
213
- if (!tableExists) {
214
- db.close();
215
- return null;
216
- }
217
- // Retrieve current_actions
218
- const stmt = db.prepare(`
219
- SELECT value FROM key_value_store
220
- WHERE key = ?
221
- `);
222
- const result = stmt.get('current_actions');
223
- db.close();
224
- if (!result) {
225
- return null;
226
- }
227
- // Parse JSON back to actions array
228
- const actions = JSON.parse(result.value);
229
- return actions;
230
- }
231
- catch (error) {
232
- console.error('Error retrieving actions:', error);
233
- return null;
298
+ sqlite = createRequire(import.meta.url)("node:sqlite");
299
+ } finally {
300
+ process.emitWarning = emitWarning;
234
301
  }
302
+ }
303
+ return sqlite;
304
+ };
305
+ var openDatabase = () => {
306
+ const db = new (loadSqlite()).DatabaseSync(databasePath());
307
+ db.exec("PRAGMA busy_timeout = 5000; PRAGMA journal_mode = WAL;");
308
+ return db;
309
+ };
310
+ var withDatabase = (fn) => {
311
+ const db = openDatabase();
312
+ try {
313
+ return fn(db);
314
+ } finally {
315
+ db.close();
316
+ }
317
+ };
318
+ var transaction = (db, fn) => {
319
+ db.exec("BEGIN IMMEDIATE");
320
+ try {
321
+ const result = fn();
322
+ db.exec("COMMIT");
323
+ return result;
324
+ } catch (error) {
325
+ db.exec("ROLLBACK");
326
+ throw error;
327
+ }
328
+ };
329
+
330
+ // src/store/keyValueStore.ts
331
+ var SCHEMA = `
332
+ CREATE TABLE IF NOT EXISTS key_value_store (
333
+ key TEXT PRIMARY KEY,
334
+ value TEXT NOT NULL,
335
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
336
+ )
337
+ `;
338
+ var setStoredValue = (key, value) => withDatabase((db) => {
339
+ db.exec(SCHEMA);
340
+ db.prepare(`INSERT OR REPLACE INTO key_value_store (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)`).run(key, value);
341
+ return databasePath();
342
+ });
343
+ var getStoredValue = (key) => withDatabase((db) => {
344
+ db.exec(SCHEMA);
345
+ const row = db.prepare(`SELECT value FROM key_value_store WHERE key = ?`).get(key);
346
+ return row ? row.value : null;
347
+ });
348
+
349
+ // src/tools/get_current_actions.ts
350
+ var getCurrentActions = () => {
351
+ try {
352
+ const value = getStoredValue(CURRENT_ACTIONS_KEY);
353
+ return value ? JSON.parse(value) : null;
354
+ } catch (error) {
355
+ console.error("Error retrieving actions:", error);
356
+ return null;
357
+ }
358
+ };
359
+ var getCurrentActionsOrThrow = () => {
360
+ const actions = getCurrentActions();
361
+ if (!actions) {
362
+ throw new Error("No actions found in storage. Please use codevideo_set_current_actions first to store actions, then try again.");
363
+ }
364
+ return actions;
235
365
  };
236
- // Helper function that returns actions or throws descriptive error
237
- const getCurrentActionsOrThrow = () => {
238
- const actions = getCurrentActions();
239
- if (!actions) {
240
- throw new Error('No actions found in storage. Please use codevideo_set_current_actions first to store actions, then try again.');
366
+
367
+ // src/prompts/instructions_to_translate_actions.ts
368
+ var instructionsToTranslateActions = (actionsToTranslation, targetLanguage) => {
369
+ let actionsToUse;
370
+ if (actionsToTranslation && actionsToTranslation.length > 0) {
371
+ actionsToUse = actionsToTranslation;
372
+ } else {
373
+ const storedActions = getCurrentActions();
374
+ if (!storedActions || storedActions.length === 0) {
375
+ return "No actions provided and no actions found in storage. Either pass actionsToTranslation parameter or use codevideo_set_current_actions first to store actions.";
241
376
  }
242
- return actions;
243
- };
244
-
245
- const getFinalLessonSnapshot = (actions) => {
246
- // Use provided actions or retrieve from storage
247
- const actionsToUse = actions || getCurrentActionsOrThrow();
248
- // create a new instance of codevideo virtual ide
249
- const virtualIDE = new VirtualIDE();
250
- // apply all actions to the virtual IDE
251
- virtualIDE.applyActions(actionsToUse);
252
- // get the final snapshot of the virtual IDE
253
- const finalSnapshot = virtualIDE.getCourseSnapshot();
254
- // return the final snapshot as a string
255
- return JSON.stringify(finalSnapshot, null, 2);
256
- };
257
-
258
- const getInitialLessonStateFromLocalCodebase = async (repoLocation) => {
259
- // build file structure from files and content from the file system adapter
260
- const fileStructure = await buildFileStructureFromRootFolder(repoLocation);
261
- // build the project using the file structure
262
- const project = {
263
- id: "",
264
- name: "",
265
- description: "",
266
- actions: [],
267
- initialSnapshot: {
268
- isUnsavedChangesDialogOpen: false,
269
- unsavedFileName: '',
270
- fileExplorerSnapshot: {
271
- isFileExplorerContextMenuOpen: false,
272
- isFileContextMenuOpen: false,
273
- isFolderContextMenuOpen: false,
274
- isNewFileInputVisible: false,
275
- isNewFolderInputVisible: false,
276
- isRenameFileInputVisible: false,
277
- isRenameFolderInputVisible: false,
278
- newFileInputValue: '',
279
- newFolderInputValue: '',
280
- renameFileInputValue: '',
281
- renameFolderInputValue: '',
282
- originalFileBeingRenamed: '',
283
- originalFolderBeingRenamed: '',
284
- newFileParentPath: '',
285
- newFolderParentPath: '',
286
- fileStructure,
287
- },
288
- editorSnapshot: {
289
- isEditorContextMenuOpen: false,
290
- editors: []
291
- },
292
- terminalSnapshot: {
293
- terminals: []
294
- },
295
- mouseSnapshot: {
296
- location: 'editor',
297
- currentHoveredFileName: '',
298
- currentHoveredFolderName: '',
299
- currentHoveredEditorTabFileName: '',
300
- x: 0,
301
- y: 0,
302
- timestamp: 0,
303
- type: 'move',
304
- buttonStates: {
305
- left: false,
306
- right: false,
307
- middle: false,
308
- },
309
- scrollPosition: {
310
- x: 0,
311
- y: 0
312
- },
313
- },
314
- authorSnapshot: {
315
- authors: [
316
- { currentSpeechCaption: '' }
317
- ]
318
- }
319
- }
320
- };
321
- // return the project as a string
322
- return JSON.stringify(project);
377
+ actionsToUse = storedActions;
378
+ }
379
+ return `## \u{1F4DD} Translation Instructions for CodeVideo Action Scripts
380
+
381
+ ### \u{1F4CC} Objective
382
+
383
+ 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**.
384
+
385
+ **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.
386
+
387
+ **VALIDATION TIP**: After translation, you can validate the translated actions using \`codevideo_validate_actions\` to ensure the CodeVideo structure remains intact.
388
+
389
+ ---
390
+
391
+ ### \u2705 What to Translate
392
+ Translate the following, only if found within the \`"value"\` fields:
393
+
394
+ 1. **All narration steps:**
395
+ - \`"name": "author-speak-before"\` or \`"author-speak-after"\`
396
+ \u2192 Fully translate narration into fluent, natural speech in the target language.
397
+
398
+ 2. **Code comments:**
399
+ - Lines starting with \`#\` or \`//\` or between \`/* ... */\`
400
+ \u2192 Translate the comment portion only, preserving syntax markers.
401
+
402
+ 3. **String literals inside code:**
403
+ - e.g., \`print("Hello, World!")\`
404
+ \u2192 Translate the string inside the quotes, preserving quote marks and code structure.
405
+
406
+ 4. **Terminal outputs:**
407
+ - \`"name": "terminal-set-output"\`
408
+ \u2192 Translate printed outputs line by line.
409
+
410
+ ---
411
+
412
+ ### \u{1F6AB} What NOT to Translate
413
+ Do **not** translate the following:
414
+
415
+ - \`"name"\` field values (e.g., \`"author-speak-before"\`, \`"editor-type"\`, etc.)
416
+ \u2192 These are CodeVideo internal commands.
417
+
418
+ - File names, identifiers, variables, or function names (e.g., \`main.py\`, \`range\`, \`print\`, \`for i in range(...)\`)
419
+ \u2192 Keep code functional and consistent.
420
+
421
+ - Keywords or syntax in programming code
422
+ \u2192 Only translate *comments* and *strings*.
423
+
424
+ ---
425
+
426
+ ### \u{1F4A1} Examples
427
+
428
+ #### Code Comment
429
+
430
+ \`\`\`json
431
+ { "name": "editor-type", "value": "# Print 'Hello, World!' five times" }
432
+ \`\`\`
433
+
434
+ \u27A1 becomes:
435
+
436
+ \`\`\`json
437
+ { "name": "editor-type", "value": "# Imprime '\xA1Hola, Mundo!' cinco veces" }
438
+ \`\`\`
439
+
440
+ #### Narration
441
+ \`\`\`json
442
+ { "name": "author-speak-before", "value": "Let's open a terminal and run the script." }
443
+ \`\`\`
444
+ \u27A1 becomes:
445
+ \`\`\`json
446
+ { "name": "author-speak-before", "value": "Vamos a abrir una terminal y ejecutar el script." }
447
+ \`\`\`
448
+
449
+ #### Terminal Output
450
+ \`\`\`json
451
+ { "name": "terminal-set-output", "value": "Hello, World!
452
+ Hello, World!" }
453
+ \`\`\`
454
+ \u27A1 becomes:
455
+ \`\`\`json
456
+ { "name": "terminal-set-output", "value": "\xA1Hola, Mundo!
457
+ \xA1Hola, Mundo!" }
458
+ \`\`\`
459
+
460
+ ---
461
+
462
+ ### \u{1F30D} Target Language
463
+
464
+ The target language will be specified separately. Use fluent, natural phrasing appropriate to that language's tone and style of instruction.
465
+
466
+ ---
467
+
468
+ ### \u{1F9E0} Tips
469
+
470
+ - Match tone to the original narration (enthusiastic, beginner-friendly).
471
+ - Avoid overly literal translations \u2014 clarity is more important.
472
+ - Maintain line breaks, indentation, and special characters exactly as they appear in code or outputs.
473
+
474
+ ---
475
+
476
+ ### \u2705 Output Format
477
+
478
+ Return the translated JSON with only the \`value\` fields updated. The JSON structure must remain valid and unchanged otherwise.
479
+
480
+ \`\`\`json
481
+ { "name": "editor-type", "value": "# Print 'Hello, World!' five times" }
482
+ \`\`\`
483
+ \u27A1 becomes:
484
+ \`\`\`json
485
+ { "name": "editor-type", "value": "# Imprime '\xA1Hola, Mundo!' cinco veces" }
486
+ \`\`\`
487
+
488
+ #### Narration
489
+ \`\`\`json
490
+ { "name": "author-speak-before", "value": "Let's open a terminal and run the script." }
491
+ \`\`\`
492
+ \u27A1 becomes:
493
+ \`\`\`json
494
+ { "name": "author-speak-before", "value": "Vamos a abrir una terminal y ejecutar el script." }
495
+ \`\`\`
496
+
497
+ #### Terminal Output
498
+ \`\`\`json
499
+ { "name": "terminal-set-output", "value": "Hello, World!
500
+ Hello, World!" }
501
+ \`\`\`
502
+ \u27A1 becomes:
503
+ \`\`\`json
504
+ { "name": "terminal-set-output", "value": "\xA1Hola, Mundo!
505
+ \xA1Hola, Mundo!" }
506
+ \`\`\`
507
+
508
+ ---
509
+
510
+ ### \u{1F30D} Target Language
511
+
512
+ The target language will be specified below. Use fluent, natural phrasing appropriate to that language's tone and style of instruction.
513
+
514
+ ---
515
+
516
+ ### \u{1F9E0} Tips
517
+
518
+ - Match tone to the original narration (enthusiastic, beginner-friendly).
519
+ - Avoid overly literal translations \u2014 clarity is more important.
520
+ - Maintain line breaks, indentation, and special characters exactly as they appear in code or outputs.
521
+
522
+ ---
523
+
524
+ ### \u2705 Output Format
525
+ Return the translated JSON with only the \`"value"\` fields updated. The JSON structure must remain valid and unchanged otherwise.
526
+
527
+
528
+ Considering all these instructions, please translate the following actions into ${targetLanguage}:
529
+
530
+ \`\`\`json
531
+ ${JSON.stringify(actionsToUse, null, 4)}
532
+ \`\`\`
533
+
534
+ `;
323
535
  };
324
536
 
325
- const isRepeatable = (action) => {
326
- return isRepeatableAction(action) ? "true" : "false";
537
+ // src/prompts/instructions_to_build_course_from_scratch.ts
538
+ var instructionsToBuildCourseFromScratch = (additionalContext) => {
539
+ return `You are an expert senior software engineer and educator building a COMPLETE, multi-lesson software course with the CodeVideo MCP, in the clear, small-step style of a polished screencast series: one concept per lesson, narrate before and while you type, and make sure the code actually builds at the end of every lesson that is meant to build.
540
+
541
+ CRITICAL ARCHITECTURE RULE \u2014 keep snapshots OUT of your context:
542
+ The course lives server-side, keyed by id. You author and read only ACTIONS, compile errors, and small targeted file reads. Do NOT fetch or pass whole snapshots or whole course/lesson objects around \u2014 that is what used to make full courses fail. Use the course store tools below; they chain each lesson's ending state into the next for you, server-side.
543
+
544
+ THE LOOP:
545
+ 1. Outline the course first: a numbered list of lessons, each teaching ONE concept, ordered so the code grows naturally from nothing to a working result.
546
+ 2. codevideo_create_course(name, description, primaryLanguage) -> returns courseId. Hold onto it.
547
+ 3. For each lesson, in order:
548
+ a. codevideo_add_lesson(courseId, { name, description, mustCompile, continuesFromPrevious: true }).
549
+ - description is required: one sentence on what the lesson teaches. The next lesson's opening recap is written from it, and the reply gives you the previous lesson's.
550
+ - Set mustCompile: false for pure slideshow/discussion lessons or deliberate half-refactors where the code is NOT expected to compile yet.
551
+ - Set mustCompile: true (the default) for any lesson that ends with working code.
552
+ - The FINAL lesson must be mustCompile: true \u2014 all code must work by the end of the course.
553
+ b. Author the actions array for this lesson. Build on the previous lesson's ending state (it is already the starting state of this lesson \u2014 you don't pass it in). If you need to know what exists, use codevideo_get_lesson_file_tree, and codevideo_get_lesson_file for a specific file's current contents \u2014 never dump everything.
554
+ c. codevideo_set_lesson_actions(courseId, lessonId, actions). The server recomputes the lesson's final state.
555
+ d. If this lesson is mustCompile, codevideo_compile_lesson(courseId, lessonId). If there are errors, fix just the actions involved with codevideo_edit_lesson_actions (find indices with codevideo_get_lesson_actions; don't re-send the whole lesson) and repeat (d) until it compiles clean. (If the compile tool reports "pending", continue \u2014 compilation will be enforced once available.)
556
+ 4. When all lessons are authored: codevideo_validate_course(courseId). It returns "done": true only when every lesson's actions are valid, every lesson follows cleanly from the last, every mustCompile lesson compiles, and the final lesson compiles clean. If "done" is false, read "reasons" and the per-lesson results, fix, and re-validate.
557
+ 5. Use codevideo_get_course_status(courseId) at any time to see what's authored without pulling snapshots.
558
+
559
+ HARD RULE \u2014 NO VIDEO DURING AUTHORING:
560
+ For a full course, do NOT generate any video until codevideo_validate_course returns done: true. Rendering is the expensive final step. After validation passes, previews (codevideo_generate_course_previews: an HTML page per lesson) and the final videos (codevideo_render_course_videos: background render jobs, one lesson at a time; follow them with codevideo_list_render_jobs) are a separate, human-approved step \u2014 do not interleave them with authoring.
561
+
562
+ ${AUTHORING_RULES}
563
+
564
+ Now build the course described here:
565
+
566
+ ${additionalContext}
567
+ `;
327
568
  };
328
569
 
329
- const createCourseWithInitialMetadata = (id, name, description, primaryLanguage) => {
330
- const course = {
331
- id,
332
- name,
333
- description,
334
- primaryLanguage,
335
- lessons: []
336
- };
337
- return JSON.stringify(course, null, 2);
338
- };
339
-
340
- const addLessonToCourse = (course, id, name, description) => {
341
- const lesson = {
342
- id,
343
- name,
344
- description,
345
- actions: [],
346
- initialSnapshot: {
347
- isUnsavedChangesDialogOpen: false,
348
- unsavedFileName: '',
349
- fileExplorerSnapshot: {
350
- isFileExplorerContextMenuOpen: false,
351
- isFileContextMenuOpen: false,
352
- isFolderContextMenuOpen: false,
353
- isNewFileInputVisible: false,
354
- isNewFolderInputVisible: false,
355
- isRenameFileInputVisible: false,
356
- isRenameFolderInputVisible: false,
357
- newFileInputValue: '',
358
- newFolderInputValue: '',
359
- renameFileInputValue: '',
360
- renameFolderInputValue: '',
361
- originalFileBeingRenamed: '',
362
- originalFolderBeingRenamed: '',
363
- newFileParentPath: '',
364
- newFolderParentPath: '',
365
- fileStructure: {},
366
- },
367
- editorSnapshot: {
368
- isEditorContextMenuOpen: false,
369
- editors: []
370
- },
371
- terminalSnapshot: {
372
- terminals: []
373
- },
374
- mouseSnapshot: {
375
- location: 'editor',
376
- currentHoveredFileName: '',
377
- currentHoveredFolderName: '',
378
- currentHoveredEditorTabFileName: '',
379
- x: 0,
380
- y: 0,
381
- timestamp: 0,
382
- type: 'move',
383
- buttonStates: {
384
- left: false,
385
- right: false,
386
- middle: false,
387
- },
388
- scrollPosition: {
389
- x: 0,
390
- y: 0
391
- },
392
- },
393
- authorSnapshot: {
394
- authors: [
395
- { currentSpeechCaption: '' }
396
- ]
397
- }
398
- }
399
- };
400
- // Add the lesson to the course
401
- course.lessons.push(lesson);
402
- // Return the updated course object
403
- return JSON.stringify(course, null, 2);
404
- };
405
-
406
- const addActionsToLesson = (lesson, actions) => {
407
- lesson.actions = actions;
408
- return JSON.stringify(lesson, null, 2);
409
- };
410
-
411
- const getEmptyLessonSnapshot = () => {
412
- const snapshot = {
413
- isUnsavedChangesDialogOpen: false,
414
- unsavedFileName: '',
415
- fileExplorerSnapshot: {
416
- isFileExplorerContextMenuOpen: false,
417
- isFileContextMenuOpen: false,
418
- isFolderContextMenuOpen: false,
419
- isNewFileInputVisible: false,
420
- isNewFolderInputVisible: false,
421
- isRenameFileInputVisible: false,
422
- isRenameFolderInputVisible: false,
423
- newFileInputValue: '',
424
- newFolderInputValue: '',
425
- renameFileInputValue: '',
426
- renameFolderInputValue: '',
427
- originalFileBeingRenamed: '',
428
- originalFolderBeingRenamed: '',
429
- newFileParentPath: '',
430
- newFolderParentPath: '',
431
- fileStructure: {},
432
- },
433
- editorSnapshot: {
434
- isEditorContextMenuOpen: false,
435
- editors: []
570
+ // src/tools/get_final_lesson_snapshot.ts
571
+ import { VirtualIDE } from "@fullstackcraftllc/codevideo-virtual-ide";
572
+ var getFinalLessonSnapshot = (actions) => {
573
+ const actionsToUse = actions || getCurrentActionsOrThrow();
574
+ const virtualIDE = new VirtualIDE();
575
+ virtualIDE.applyActions(actionsToUse);
576
+ const finalSnapshot = virtualIDE.getCourseSnapshot();
577
+ return JSON.stringify(finalSnapshot, null, 2);
578
+ };
579
+
580
+ // src/tools/get_initial_lesson_state_from_local_repo.ts
581
+ import { buildFileStructureFromRootFolder } from "@fullstackcraftllc/codevideo-adapters";
582
+ var getInitialLessonStateFromLocalCodebase = async (repoLocation) => {
583
+ const { fileStructure } = await buildFileStructureFromRootFolder(repoLocation);
584
+ const project = {
585
+ id: "",
586
+ name: "",
587
+ description: "",
588
+ actions: [],
589
+ initialSnapshot: {
590
+ isUnsavedChangesDialogOpen: false,
591
+ unsavedFileName: "",
592
+ fileExplorerSnapshot: {
593
+ isFileExplorerContextMenuOpen: false,
594
+ isFileContextMenuOpen: false,
595
+ isFolderContextMenuOpen: false,
596
+ isNewFileInputVisible: false,
597
+ isNewFolderInputVisible: false,
598
+ isRenameFileInputVisible: false,
599
+ isRenameFolderInputVisible: false,
600
+ newFileInputValue: "",
601
+ newFolderInputValue: "",
602
+ renameFileInputValue: "",
603
+ renameFolderInputValue: "",
604
+ originalFileBeingRenamed: "",
605
+ originalFolderBeingRenamed: "",
606
+ newFileParentPath: "",
607
+ newFolderParentPath: "",
608
+ fileStructure
609
+ },
610
+ editorSnapshot: {
611
+ isEditorContextMenuOpen: false,
612
+ editors: []
613
+ },
614
+ terminalSnapshot: {
615
+ terminals: []
616
+ },
617
+ mouseSnapshot: {
618
+ location: "editor",
619
+ currentHoveredFileName: "",
620
+ currentHoveredFolderName: "",
621
+ currentHoveredEditorTabFileName: "",
622
+ x: 0,
623
+ y: 0,
624
+ timestamp: 0,
625
+ type: "move",
626
+ buttonStates: {
627
+ left: false,
628
+ right: false,
629
+ middle: false
436
630
  },
437
- terminalSnapshot: {
438
- terminals: []
631
+ scrollPosition: {
632
+ x: 0,
633
+ y: 0
634
+ }
635
+ },
636
+ authorSnapshot: {
637
+ authors: [
638
+ { currentSpeechCaption: "" }
639
+ ]
640
+ }
641
+ }
642
+ };
643
+ return JSON.stringify(project);
644
+ };
645
+
646
+ // src/tools/get_initial_lesson_state_from_github_repo.ts
647
+ import { buildFileStructureFromGitHub } from "@fullstackcraftllc/codevideo-adapters";
648
+ var getInitialLessonStateFromGitHubRepo = async (repoUrl) => {
649
+ const fileStructure = await buildFileStructureFromGitHub(repoUrl);
650
+ const project = {
651
+ id: "",
652
+ name: "",
653
+ description: "",
654
+ actions: [],
655
+ initialSnapshot: {
656
+ isUnsavedChangesDialogOpen: false,
657
+ unsavedFileName: "",
658
+ fileExplorerSnapshot: {
659
+ isFileExplorerContextMenuOpen: false,
660
+ isFileContextMenuOpen: false,
661
+ isFolderContextMenuOpen: false,
662
+ isNewFileInputVisible: false,
663
+ isNewFolderInputVisible: false,
664
+ isRenameFileInputVisible: false,
665
+ isRenameFolderInputVisible: false,
666
+ newFileInputValue: "",
667
+ newFolderInputValue: "",
668
+ renameFileInputValue: "",
669
+ renameFolderInputValue: "",
670
+ originalFileBeingRenamed: "",
671
+ originalFolderBeingRenamed: "",
672
+ newFileParentPath: "",
673
+ newFolderParentPath: "",
674
+ fileStructure
675
+ },
676
+ editorSnapshot: {
677
+ isEditorContextMenuOpen: false,
678
+ editors: []
679
+ },
680
+ terminalSnapshot: {
681
+ terminals: []
682
+ },
683
+ mouseSnapshot: {
684
+ location: "editor",
685
+ currentHoveredFileName: "",
686
+ currentHoveredFolderName: "",
687
+ currentHoveredEditorTabFileName: "",
688
+ x: 0,
689
+ y: 0,
690
+ timestamp: 0,
691
+ type: "move",
692
+ buttonStates: {
693
+ left: false,
694
+ right: false,
695
+ middle: false
439
696
  },
440
- mouseSnapshot: {
441
- location: 'editor',
442
- currentHoveredFileName: '',
443
- currentHoveredFolderName: '',
444
- currentHoveredEditorTabFileName: '',
445
- x: 0,
446
- y: 0,
447
- timestamp: 0,
448
- type: 'move',
449
- buttonStates: {
450
- left: false,
451
- right: false,
452
- middle: false,
453
- },
454
- scrollPosition: {
455
- x: 0,
456
- y: 0
457
- },
697
+ scrollPosition: {
698
+ x: 0,
699
+ y: 0
700
+ }
701
+ },
702
+ authorSnapshot: {
703
+ authors: [
704
+ { currentSpeechCaption: "" }
705
+ ]
706
+ }
707
+ }
708
+ };
709
+ return JSON.stringify(project);
710
+ };
711
+
712
+ // src/tools/is_repeatable_action.ts
713
+ import { isRepeatableAction } from "@fullstackcraftllc/codevideo-types";
714
+ var isRepeatable = (action) => {
715
+ return isRepeatableAction(action) ? "true" : "false";
716
+ };
717
+
718
+ // src/tools/create_course_with_initial_metadata.ts
719
+ var createCourseWithInitialMetadata = (id, name, description, primaryLanguage) => {
720
+ const course = {
721
+ id,
722
+ name,
723
+ description,
724
+ primaryLanguage,
725
+ lessons: []
726
+ };
727
+ return JSON.stringify(course, null, 2);
728
+ };
729
+
730
+ // src/tools/add_lesson_to_course.ts
731
+ var addLessonToCourse = (course, id, name, description) => {
732
+ const lesson = {
733
+ id,
734
+ name,
735
+ description,
736
+ actions: [],
737
+ initialSnapshot: {
738
+ isUnsavedChangesDialogOpen: false,
739
+ unsavedFileName: "",
740
+ fileExplorerSnapshot: {
741
+ isFileExplorerContextMenuOpen: false,
742
+ isFileContextMenuOpen: false,
743
+ isFolderContextMenuOpen: false,
744
+ isNewFileInputVisible: false,
745
+ isNewFolderInputVisible: false,
746
+ isRenameFileInputVisible: false,
747
+ isRenameFolderInputVisible: false,
748
+ newFileInputValue: "",
749
+ newFolderInputValue: "",
750
+ renameFileInputValue: "",
751
+ renameFolderInputValue: "",
752
+ originalFileBeingRenamed: "",
753
+ originalFolderBeingRenamed: "",
754
+ newFileParentPath: "",
755
+ newFolderParentPath: "",
756
+ fileStructure: {}
757
+ },
758
+ editorSnapshot: {
759
+ isEditorContextMenuOpen: false,
760
+ editors: []
761
+ },
762
+ terminalSnapshot: {
763
+ terminals: []
764
+ },
765
+ mouseSnapshot: {
766
+ location: "editor",
767
+ currentHoveredFileName: "",
768
+ currentHoveredFolderName: "",
769
+ currentHoveredEditorTabFileName: "",
770
+ x: 0,
771
+ y: 0,
772
+ timestamp: 0,
773
+ type: "move",
774
+ buttonStates: {
775
+ left: false,
776
+ right: false,
777
+ middle: false
458
778
  },
459
- authorSnapshot: {
460
- authors: [
461
- { currentSpeechCaption: '' }
462
- ]
779
+ scrollPosition: {
780
+ x: 0,
781
+ y: 0
463
782
  }
464
- };
465
- // return the snapshot as a string
466
- return JSON.stringify(snapshot, null, 2);
783
+ },
784
+ authorSnapshot: {
785
+ authors: [
786
+ { currentSpeechCaption: "" }
787
+ ]
788
+ }
789
+ }
790
+ };
791
+ course.lessons.push(lesson);
792
+ return JSON.stringify(course, null, 2);
793
+ };
794
+
795
+ // src/tools/add_actions_to_lesson.ts
796
+ var addActionsToLesson = (lesson, actions) => {
797
+ lesson.actions = actions;
798
+ return JSON.stringify(lesson, null, 2);
467
799
  };
468
800
 
469
- const critiqueVideoUsingGemini = async (videoPath, actions) => {
470
- // Use provided actions or retrieve from storage
471
- const actionsToUse = actions || getCurrentActionsOrThrow();
472
- // Check if GEMINI_API_KEY is set in the environment
473
- const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
474
- if (!GEMINI_API_KEY) {
475
- throw new Error('GEMINI_API_KEY environment variable is not set');
801
+ // src/tools/get_empty_lesson_snapshot.ts
802
+ var getEmptyLessonSnapshot = () => {
803
+ const snapshot = {
804
+ isUnsavedChangesDialogOpen: false,
805
+ unsavedFileName: "",
806
+ fileExplorerSnapshot: {
807
+ isFileExplorerContextMenuOpen: false,
808
+ isFileContextMenuOpen: false,
809
+ isFolderContextMenuOpen: false,
810
+ isNewFileInputVisible: false,
811
+ isNewFolderInputVisible: false,
812
+ isRenameFileInputVisible: false,
813
+ isRenameFolderInputVisible: false,
814
+ newFileInputValue: "",
815
+ newFolderInputValue: "",
816
+ renameFileInputValue: "",
817
+ renameFolderInputValue: "",
818
+ originalFileBeingRenamed: "",
819
+ originalFolderBeingRenamed: "",
820
+ newFileParentPath: "",
821
+ newFolderParentPath: "",
822
+ fileStructure: {}
823
+ },
824
+ editorSnapshot: {
825
+ isEditorContextMenuOpen: false,
826
+ editors: []
827
+ },
828
+ terminalSnapshot: {
829
+ terminals: []
830
+ },
831
+ mouseSnapshot: {
832
+ location: "editor",
833
+ currentHoveredFileName: "",
834
+ currentHoveredFolderName: "",
835
+ currentHoveredEditorTabFileName: "",
836
+ x: 0,
837
+ y: 0,
838
+ timestamp: 0,
839
+ type: "move",
840
+ buttonStates: {
841
+ left: false,
842
+ right: false,
843
+ middle: false
844
+ },
845
+ scrollPosition: {
846
+ x: 0,
847
+ y: 0
848
+ }
849
+ },
850
+ authorSnapshot: {
851
+ authors: [
852
+ { currentSpeechCaption: "" }
853
+ ]
476
854
  }
477
- // Initialize the Gemini API client
478
- const ai = new GoogleGenAI({ apiKey: GEMINI_API_KEY });
479
- const prompt = `You are an expert senior software engineer and educator, and skilled in describing software concepts in a way that is easy for students to understand. You are going to critique a procedurally generated software video lesson for clarity, accuracy, and correctness. The video and the lesson actions are attached. Your tasks:
855
+ };
856
+ return JSON.stringify(snapshot, null, 2);
857
+ };
858
+
859
+ // src/tools/critique_video_using_gemini.ts
860
+ import { GoogleGenAI } from "@google/genai";
861
+ import fs2 from "fs";
862
+ var critiqueVideoUsingGemini = async (videoPath, actions) => {
863
+ const actionsToUse = actions || getCurrentActionsOrThrow();
864
+ const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
865
+ if (!GEMINI_API_KEY) {
866
+ throw new Error("GEMINI_API_KEY environment variable is not set");
867
+ }
868
+ const ai = new GoogleGenAI({ apiKey: GEMINI_API_KEY });
869
+ const prompt = `You are an expert senior software engineer and educator, and skilled in describing software concepts in a way that is easy for students to understand. You are going to critique a procedurally generated software video lesson for clarity, accuracy, and correctness. The video and the lesson actions are attached. Your tasks:
480
870
 
481
871
  1. Watch the video and read the lesson actions.
482
872
  2. Critique the video for clarity, accuracy, and correctness. Of particular note is any audio artifacts or strange wordings. Do not suggest any new material, only critique the existing material.
483
873
  3. Provide a summary of your critique, including any errors or inaccuracies you found, and at what action they occur at.`;
484
- try {
485
- // Read the video file as a binary blob for the API
486
- const videoBuffer = fs.readFileSync(videoPath);
487
- const videoBase64 = videoBuffer.toString('base64');
488
- // Convert actions array to JSON string
489
- const actionsJson = JSON.stringify(actionsToUse, null, 2);
490
- // Prepare the request with content parts including text and file data
491
- const response = await ai.models.generateContent({
492
- model: 'gemini-2.5-pro-preview-05-06',
493
- contents: [
494
- {
495
- role: 'user',
496
- parts: [
497
- { text: prompt },
498
- {
499
- fileData: {
500
- mimeType: 'video/mp4',
501
- fileUri: `data:video/mp4;base64,${videoBase64}`
502
- }
503
- },
504
- {
505
- text: `Here are the lesson actions in JSON format:\n\n${actionsJson}`
506
- }
507
- ]
508
- }
509
- ]
510
- });
511
- // Get the response text
512
- const result = response.text;
513
- return result || "No critique was generated";
514
- }
515
- catch (error) {
516
- console.error('Error during Gemini API call:', error);
517
- const errorMessage = error instanceof Error ? error.message : String(error);
518
- throw new Error(`Failed to critique video using Gemini: ${errorMessage}`);
519
- }
520
- };
874
+ try {
875
+ const videoBuffer = fs2.readFileSync(videoPath);
876
+ const videoBase64 = videoBuffer.toString("base64");
877
+ const actionsJson = JSON.stringify(actionsToUse, null, 2);
878
+ const response = await ai.models.generateContent({
879
+ model: "gemini-2.5-pro-preview-05-06",
880
+ contents: [
881
+ {
882
+ role: "user",
883
+ parts: [
884
+ { text: prompt },
885
+ {
886
+ fileData: {
887
+ mimeType: "video/mp4",
888
+ fileUri: `data:video/mp4;base64,${videoBase64}`
889
+ }
890
+ },
891
+ {
892
+ text: `Here are the lesson actions in JSON format:
521
893
 
522
- // sets the current actions to the key value sqlite database
523
- const setCurrentActions = (actions) => {
524
- try {
525
- // Create database directory if it doesn't exist
526
- const homeDir = os.homedir();
527
- const dbDir = path.join(homeDir, '.codevideo');
528
- if (!fs.existsSync(dbDir)) {
529
- fs.mkdirSync(dbDir, { recursive: true });
894
+ ${actionsJson}`
895
+ }
896
+ ]
530
897
  }
531
- const dbPath = path.join(dbDir, 'codevideo.db');
532
- // Open or create database
533
- const db = new Database(dbPath);
534
- // Create table if it doesn't exist
535
- db.exec(`
536
- CREATE TABLE IF NOT EXISTS key_value_store (
537
- key TEXT PRIMARY KEY,
538
- value TEXT NOT NULL,
539
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
540
- )
541
- `);
542
- // Prepare statement to insert or replace the current_actions
543
- const stmt = db.prepare(`
544
- INSERT OR REPLACE INTO key_value_store (key, value, updated_at)
545
- VALUES (?, ?, CURRENT_TIMESTAMP)
546
- `);
547
- // Convert actions to JSON string and store
548
- const actionsJson = JSON.stringify(actions);
549
- stmt.run('current_actions', actionsJson);
550
- db.close();
551
- return `Successfully stored ${actions.length} actions in local database at ${dbPath}`;
552
- }
553
- catch (error) {
554
- return `Error storing actions: ${error.message}`;
555
- }
898
+ ]
899
+ });
900
+ const result = response.text;
901
+ return result || "No critique was generated";
902
+ } catch (error) {
903
+ console.error("Error during Gemini API call:", error);
904
+ const errorMessage = error instanceof Error ? error.message : String(error);
905
+ throw new Error(`Failed to critique video using Gemini: ${errorMessage}`);
906
+ }
556
907
  };
557
908
 
558
- // calls the markdown generator from the codevideo-exporters package
559
- const makeBlogPost = (actions) => {
560
- // Use provided actions or retrieve from storage
561
- const actionsToUse = actions || getCurrentActionsOrThrow();
562
- return generateMarkdownStringFromActions(actionsToUse);
563
- };
564
-
565
- // calls the HTML generator from the codevideo-exporters package
566
- const makeHTMLWebPage = async (actions) => {
567
- // Use provided actions or retrieve from storage
568
- const actionsToUse = actions || getCurrentActionsOrThrow();
569
- // Generate the HTML content
570
- const htmlContent = await generateHtmlStringFromActions(actionsToUse);
571
- // Create output file on desktop with timestamp
572
- const homeDir = os.homedir();
573
- const desktopDir = path.join(homeDir, 'Desktop');
574
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
575
- const outputFile = path.join(desktopDir, `codevideo-${timestamp}.html`);
576
- // Write HTML to file
577
- fs.writeFileSync(outputFile, htmlContent, 'utf8');
578
- // Open the HTML file in the default browser
579
- const openChild = spawn('open', [outputFile], {
580
- detached: true,
581
- stdio: 'ignore'
582
- });
583
- openChild.unref();
584
- // Return the HTML content
585
- return htmlContent;
909
+ // src/tools/set_current_actions.ts
910
+ var setCurrentActions = (actions) => {
911
+ try {
912
+ const dbPath = setStoredValue(CURRENT_ACTIONS_KEY, JSON.stringify(actions));
913
+ return `Successfully stored ${actions.length} actions in local database at ${dbPath}`;
914
+ } catch (error) {
915
+ return `Error storing actions: ${error.message}`;
916
+ }
586
917
  };
587
918
 
588
- function getBinaryPath() {
589
- const platformMap = {
590
- 'darwin': 'darwin',
591
- 'linux': 'linux',
592
- 'win32': 'win32'
593
- };
594
- const archMap = {
595
- 'x64': 'x64',
596
- 'arm64': 'arm64'
597
- };
598
- const targetPlatform = platformMap[platform()];
599
- const targetArch = archMap[arch()];
600
- if (!targetPlatform || !targetArch) {
601
- throw new Error(`Unsupported platform: ${platform()}-${arch()}`);
602
- }
603
- const binaryName = platform() === 'win32' ? 'codevideo-cli.exe' : 'codevideo-cli';
604
- const binaryPath = join(__dirname, '..', 'bin', `${targetPlatform}-${targetArch}`, binaryName);
605
- if (!existsSync(binaryPath)) {
606
- throw new Error(`Binary not found for ${targetPlatform}-${targetArch} at ${binaryPath}`);
919
+ // src/tools/set_current_lesson.ts
920
+ var setCurrentLesson = (lesson) => {
921
+ try {
922
+ const dbPath = setStoredValue(CURRENT_LESSON_KEY, JSON.stringify(lesson));
923
+ return `Successfully stored lesson "${lesson.name}" (ID: ${lesson.id}) in local database at ${dbPath}`;
924
+ } catch (error) {
925
+ return `Error storing lesson: ${error.message}`;
926
+ }
927
+ };
928
+
929
+ // src/tools/get_current_lesson.ts
930
+ var getCurrentLesson = () => {
931
+ try {
932
+ const value = getStoredValue(CURRENT_LESSON_KEY);
933
+ return value ? JSON.parse(value) : null;
934
+ } catch (error) {
935
+ console.error("Error retrieving lesson:", error);
936
+ return null;
937
+ }
938
+ };
939
+ var getCurrentLessonOrThrow = () => {
940
+ const lesson = getCurrentLesson();
941
+ if (!lesson) {
942
+ throw new Error("No lesson found in storage. Please use codevideo_set_current_lesson first to store a lesson, then try again.");
943
+ }
944
+ return lesson;
945
+ };
946
+
947
+ // src/tools/make_markdown_from_actions.ts
948
+ import { generateMarkdownStringFromActions } from "@fullstackcraftllc/codevideo-exporters";
949
+ var makeMarkdownFromActions = (actions) => {
950
+ const actionsToUse = actions || getCurrentActionsOrThrow();
951
+ return generateMarkdownStringFromActions(actionsToUse);
952
+ };
953
+
954
+ // src/tools/make_markdown_from_lesson.ts
955
+ import { generateMarkdownStringFromActions as generateMarkdownStringFromActions2 } from "@fullstackcraftllc/codevideo-exporters";
956
+ var makeMarkdownFromLesson = (lesson) => {
957
+ const lessonToUse = lesson || getCurrentLesson();
958
+ if (!lessonToUse) {
959
+ throw new Error("No lesson provided and no current lesson found in storage");
960
+ }
961
+ return generateMarkdownStringFromActions2(lessonToUse.actions);
962
+ };
963
+
964
+ // src/tools/make_html_web_page.ts
965
+ import os2 from "os";
966
+ import path2 from "path";
967
+ import fs3 from "fs";
968
+ import { spawn } from "child_process";
969
+ import { generateHtmlStringFromActions } from "@fullstackcraftllc/codevideo-exporters";
970
+ var makeHTMLWebPage = async (actions) => {
971
+ const actionsToUse = actions || getCurrentActionsOrThrow();
972
+ const htmlContent = await generateHtmlStringFromActions(actionsToUse, {});
973
+ const homeDir = os2.homedir();
974
+ const desktopDir = path2.join(homeDir, "Desktop");
975
+ const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
976
+ const outputFile = path2.join(desktopDir, `codevideo-${timestamp2}.html`);
977
+ fs3.writeFileSync(outputFile, htmlContent, "utf8");
978
+ const openChild = spawn("open", [outputFile], {
979
+ detached: true,
980
+ stdio: "ignore"
981
+ });
982
+ openChild.unref();
983
+ return htmlContent;
984
+ };
985
+
986
+ // src/render/queue.ts
987
+ import fs6 from "node:fs";
988
+ import os3 from "node:os";
989
+ import path6 from "node:path";
990
+ import { estimateVideoDurationInSeconds } from "@fullstackcraftllc/codevideo-types";
991
+
992
+ // src/render/jobs.ts
993
+ import fs4 from "node:fs";
994
+ import path4 from "node:path";
995
+ import { randomUUID } from "node:crypto";
996
+
997
+ // src/render/cli.ts
998
+ import path3 from "node:path";
999
+
1000
+ // src/render/options.ts
1001
+ import { z } from "zod";
1002
+ var RenderOptionsSchema = z.object({
1003
+ orientation: z.enum(["landscape", "portrait"]).optional().describe("Video orientation; portrait renders 1080x1920. For portrait, usually also set isFileExplorerVisible, isEditorTabsVisible, isMinimapVisible and isMouseVisible to false."),
1004
+ theme: z.enum(["light", "dark"]).optional(),
1005
+ withCaptions: z.boolean().optional(),
1006
+ fontSizePx: z.number().positive().optional(),
1007
+ fileExplorerWidth: z.number().positive().optional(),
1008
+ terminalHeight: z.number().positive().optional(),
1009
+ mouseColor: z.string().min(1).optional(),
1010
+ isFileExplorerVisible: z.boolean().optional(),
1011
+ isTerminalVisible: z.boolean().optional(),
1012
+ isEditorTabsVisible: z.boolean().optional(),
1013
+ isMinimapVisible: z.boolean().optional(),
1014
+ isMouseVisible: z.boolean().optional().describe("Draw the mouse cursor. Mouse actions keep their timing and effects when it's hidden."),
1015
+ wordWrap: z.enum(["on", "off"]).optional(),
1016
+ keyboardTypingPauseMs: z.number().int().positive().optional(),
1017
+ standardPauseMs: z.number().int().positive().optional(),
1018
+ longPauseMs: z.number().int().positive().optional(),
1019
+ defaultLanguage: z.string().min(1).optional(),
1020
+ voiceId: z.string().min(1).optional().describe("ElevenLabs voice ID for the narration."),
1021
+ ttsProvider: z.enum(["elevenlabs", "kokoro"]).optional(),
1022
+ renderMode: z.enum(["realtime", "deterministic"]).optional().describe("realtime (default) records the browser as it plays; deterministic renders frame by frame."),
1023
+ renderSeed: z.string().min(1).optional().describe("Deterministic renders only: a seed for reproducible output."),
1024
+ renderWorkers: z.number().int().min(1).max(32).optional().describe("Deterministic renders only: browser workers, 1-32.")
1025
+ }).strict().refine((o) => o.renderSeed === void 0 && o.renderWorkers === void 0 || o.renderMode === "deterministic", {
1026
+ message: 'renderSeed and renderWorkers need renderMode: "deterministic"'
1027
+ });
1028
+ var IDE_PROP_KEYS = [
1029
+ "theme",
1030
+ "withCaptions",
1031
+ "fontSizePx",
1032
+ "fileExplorerWidth",
1033
+ "terminalHeight",
1034
+ "mouseColor",
1035
+ "isFileExplorerVisible",
1036
+ "isTerminalVisible",
1037
+ "isEditorTabsVisible",
1038
+ "isMinimapVisible",
1039
+ "isMouseVisible",
1040
+ "wordWrap",
1041
+ "keyboardTypingPauseMs",
1042
+ "standardPauseMs",
1043
+ "longPauseMs",
1044
+ "defaultLanguage"
1045
+ ];
1046
+ var RENDER_PAGE_PAUSES = { keyboardTypingPauseMs: 40, standardPauseMs: 200 };
1047
+ var estimatePauses = (options = {}) => ({
1048
+ keyboardTypingPauseMs: options.keyboardTypingPauseMs ?? RENDER_PAGE_PAUSES.keyboardTypingPauseMs,
1049
+ standardPauseMs: options.standardPauseMs ?? RENDER_PAGE_PAUSES.standardPauseMs
1050
+ });
1051
+
1052
+ // src/render/cli.ts
1053
+ var maxProjectArgumentBytes = (platform = process.platform) => platform === "win32" ? 3e4 : 120 * 1024;
1054
+ var ideConfigFor = (options, defaultLanguage) => {
1055
+ const props = {};
1056
+ for (const key of IDE_PROP_KEYS) {
1057
+ if (options[key] !== void 0) props[key] = options[key];
1058
+ }
1059
+ if (Object.keys(props).length === 0) return null;
1060
+ return { theme: "dark", defaultLanguage, withCaptions: true, ...props };
1061
+ };
1062
+ var cliArgs = (input) => {
1063
+ const args = ["-p", input.projectJson, "-o", input.outputPath];
1064
+ if (input.configPath) args.push("-c", input.configPath);
1065
+ if (input.options.orientation === "portrait") args.push("-n", "portrait");
1066
+ if (input.options.renderMode) args.push("--render-mode", input.options.renderMode);
1067
+ if (input.options.renderSeed) args.push("--render-seed", input.options.renderSeed);
1068
+ if (input.options.renderWorkers) args.push("--render-workers", String(input.options.renderWorkers));
1069
+ if (input.open) args.push("--open");
1070
+ return args;
1071
+ };
1072
+ var cliEnv = (base, options, nodePath = process.execPath) => {
1073
+ const env = { ...base };
1074
+ const pathKey = Object.keys(env).find((key) => key.toUpperCase() === "PATH") ?? "PATH";
1075
+ env[pathKey] = [path3.dirname(nodePath), env[pathKey]].filter(Boolean).join(path3.delimiter);
1076
+ if (options.voiceId) env.ELEVEN_LABS_VOICE_ID = options.voiceId;
1077
+ if (options.ttsProvider) env.CODEVIDEO_TTS_PROVIDER = options.ttsProvider;
1078
+ return env;
1079
+ };
1080
+ var PROGRESS = /^\[\s*\d+%\] /;
1081
+ var ERROR = /^error: (.+)$/;
1082
+ var SAVED = /^✅ CodeVideo successfully generated and saved to (.+)$/;
1083
+ var parseCliLine = (line) => {
1084
+ const text = line.trimEnd();
1085
+ if (PROGRESS.test(text)) return { kind: "progress", text };
1086
+ const error = ERROR.exec(text);
1087
+ if (error) return { kind: "error", text: error[1] };
1088
+ const saved = SAVED.exec(text);
1089
+ if (saved) return { kind: "saved", path: saved[1] };
1090
+ return { kind: "other" };
1091
+ };
1092
+
1093
+ // src/render/jobs.ts
1094
+ var SCHEMA2 = `
1095
+ CREATE TABLE IF NOT EXISTS render_jobs (
1096
+ id TEXT PRIMARY KEY,
1097
+ kind TEXT NOT NULL,
1098
+ course_id TEXT,
1099
+ lesson_id TEXT,
1100
+ label TEXT NOT NULL DEFAULT '',
1101
+ output_path TEXT NOT NULL,
1102
+ options TEXT NOT NULL DEFAULT '{}',
1103
+ open INTEGER NOT NULL DEFAULT 0,
1104
+ status TEXT NOT NULL,
1105
+ pid INTEGER,
1106
+ progress TEXT,
1107
+ error TEXT,
1108
+ cancel_requested INTEGER NOT NULL DEFAULT 0,
1109
+ created_at TEXT NOT NULL,
1110
+ started_at TEXT,
1111
+ finished_at TEXT
1112
+ );
1113
+ CREATE INDEX IF NOT EXISTS idx_render_jobs_status ON render_jobs(status);
1114
+ CREATE TABLE IF NOT EXISTS render_worker (
1115
+ id INTEGER PRIMARY KEY CHECK (id = 1),
1116
+ pid INTEGER NOT NULL,
1117
+ heartbeat_at INTEGER NOT NULL
1118
+ );
1119
+ `;
1120
+ var WORKER_STALE_MS = 1e4;
1121
+ var withJobs = (fn) => withDatabase((db) => {
1122
+ db.exec(SCHEMA2);
1123
+ return fn(db);
1124
+ });
1125
+ var jobDir = (id) => path4.join(codevideoHome(), "jobs", id);
1126
+ var toJob = (row) => ({
1127
+ id: row.id,
1128
+ kind: row.kind,
1129
+ courseId: row.course_id,
1130
+ lessonId: row.lesson_id,
1131
+ label: row.label,
1132
+ outputPath: row.output_path,
1133
+ options: JSON.parse(row.options),
1134
+ open: row.open === 1,
1135
+ status: row.status,
1136
+ pid: row.pid,
1137
+ progress: row.progress,
1138
+ error: row.error,
1139
+ cancelRequested: row.cancel_requested === 1,
1140
+ createdAt: row.created_at,
1141
+ startedAt: row.started_at,
1142
+ finishedAt: row.finished_at
1143
+ });
1144
+ var now = () => (/* @__PURE__ */ new Date()).toISOString();
1145
+ var enqueueJobs = (specs) => {
1146
+ const prepared = specs.map((spec) => {
1147
+ const projectJson = JSON.stringify(spec.project);
1148
+ const bytes = Buffer.byteLength(projectJson);
1149
+ if (bytes > maxProjectArgumentBytes()) {
1150
+ throw new Error(
1151
+ `"${spec.label}" is ${Math.ceil(bytes / 1024)} KiB as JSON, more than the ${Math.floor(maxProjectArgumentBytes() / 1024)} KiB the CLI can take on its command line here. Split it into smaller lessons.`
1152
+ );
607
1153
  }
608
- return binaryPath;
1154
+ return { spec, projectJson, id: randomUUID() };
1155
+ });
1156
+ for (const { spec, projectJson, id } of prepared) {
1157
+ const dir = jobDir(id);
1158
+ fs4.mkdirSync(dir, { recursive: true });
1159
+ fs4.writeFileSync(path4.join(dir, "project.json"), projectJson);
1160
+ const config = ideConfigFor(spec.options, spec.defaultLanguage);
1161
+ if (config) fs4.writeFileSync(path4.join(dir, "config.json"), JSON.stringify(config, null, 2));
1162
+ }
1163
+ return withJobs(
1164
+ (db) => transaction(db, () => {
1165
+ const insert = db.prepare(`
1166
+ INSERT INTO render_jobs (id, kind, course_id, lesson_id, label, output_path, options, open, status, created_at)
1167
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?)
1168
+ `);
1169
+ return prepared.map(({ spec, id }) => {
1170
+ insert.run(id, spec.kind, spec.courseId ?? null, spec.lessonId ?? null, spec.label, spec.outputPath, JSON.stringify(spec.options), spec.open ? 1 : 0, now());
1171
+ return toJob(db.prepare(`SELECT * FROM render_jobs WHERE id = ?`).get(id));
1172
+ });
1173
+ })
1174
+ );
1175
+ };
1176
+ var getJob = (id) => withJobs((db) => {
1177
+ const row = db.prepare(`SELECT * FROM render_jobs WHERE id = ?`).get(id);
1178
+ return row ? toJob(row) : null;
1179
+ });
1180
+ var listJobs = (filter = {}) => withJobs((db) => {
1181
+ const where = [];
1182
+ const params = [];
1183
+ if (filter.courseId) {
1184
+ where.push("course_id = ?");
1185
+ params.push(filter.courseId);
1186
+ }
1187
+ if (filter.status) {
1188
+ where.push("status = ?");
1189
+ params.push(filter.status);
1190
+ }
1191
+ const sql = `SELECT * FROM render_jobs ${where.length ? `WHERE ${where.join(" AND ")}` : ""} ORDER BY rowid DESC LIMIT ?`;
1192
+ return db.prepare(sql).all(...params, filter.limit ?? 20).map(toJob);
1193
+ });
1194
+ var queuePosition = (id) => withJobs((db) => {
1195
+ const row = db.prepare(`SELECT rowid AS seq, status FROM render_jobs WHERE id = ?`).get(id);
1196
+ if (!row || row.status !== "queued") return null;
1197
+ const ahead = db.prepare(`SELECT COUNT(*) AS n FROM render_jobs WHERE status = 'queued' AND rowid < ?`).get(row.seq);
1198
+ return ahead.n;
1199
+ });
1200
+ var requestCancel = (target) => withJobs(
1201
+ (db) => transaction(db, () => {
1202
+ const [column, value] = target.jobId ? ["id", target.jobId] : ["course_id", target.courseId ?? ""];
1203
+ const cancelled = db.prepare(`UPDATE render_jobs SET status = 'cancelled', finished_at = ? WHERE ${column} = ? AND status = 'queued'`).run(now(), value).changes;
1204
+ const stopping = db.prepare(`UPDATE render_jobs SET cancel_requested = 1 WHERE ${column} = ? AND status = 'running'`).run(value).changes;
1205
+ return { cancelled: Number(cancelled), stopping: Number(stopping) };
1206
+ })
1207
+ );
1208
+ var claimNextJob = () => withJobs(
1209
+ (db) => transaction(db, () => {
1210
+ const row = db.prepare(`SELECT * FROM render_jobs WHERE status = 'queued' ORDER BY rowid LIMIT 1`).get();
1211
+ if (!row) return null;
1212
+ db.prepare(`UPDATE render_jobs SET status = 'running', started_at = ? WHERE id = ?`).run(now(), row.id);
1213
+ return toJob({ ...row, status: "running" });
1214
+ })
1215
+ );
1216
+ var hasQueuedJobs = () => withJobs((db) => !!db.prepare(`SELECT 1 FROM render_jobs WHERE status = 'queued' LIMIT 1`).get());
1217
+ var setJobPid = (id, pid) => withJobs((db) => {
1218
+ db.prepare(`UPDATE render_jobs SET pid = ? WHERE id = ?`).run(pid, id);
1219
+ });
1220
+ var setJobProgress = (id, progress) => withJobs((db) => {
1221
+ db.prepare(`UPDATE render_jobs SET progress = ? WHERE id = ?`).run(progress, id);
1222
+ });
1223
+ var isCancelRequested = (id) => withJobs((db) => !!db.prepare(`SELECT 1 FROM render_jobs WHERE id = ? AND cancel_requested = 1`).get(id));
1224
+ var finishJob = (id, status, error) => withJobs((db) => {
1225
+ db.prepare(`UPDATE render_jobs SET status = ?, error = ?, finished_at = ? WHERE id = ?`).run(status, error, now(), id);
1226
+ });
1227
+ var isProcessAlive = (pid) => {
1228
+ try {
1229
+ process.kill(pid, 0);
1230
+ return true;
1231
+ } catch (error) {
1232
+ return error.code === "EPERM";
1233
+ }
1234
+ };
1235
+ var liveWorker = () => withJobs((db) => {
1236
+ const row = db.prepare(`SELECT pid, heartbeat_at FROM render_worker WHERE id = 1`).get();
1237
+ if (!row || Date.now() - row.heartbeat_at > WORKER_STALE_MS || !isProcessAlive(row.pid)) return null;
1238
+ return { pid: row.pid };
1239
+ });
1240
+ var acquireWorkerLock = (pid) => withJobs(
1241
+ (db) => transaction(db, () => {
1242
+ const row = db.prepare(`SELECT pid, heartbeat_at FROM render_worker WHERE id = 1`).get();
1243
+ if (row && row.pid !== pid && Date.now() - row.heartbeat_at <= WORKER_STALE_MS && isProcessAlive(row.pid)) return false;
1244
+ db.prepare(`INSERT OR REPLACE INTO render_worker (id, pid, heartbeat_at) VALUES (1, ?, ?)`).run(pid, Date.now());
1245
+ return true;
1246
+ })
1247
+ );
1248
+ var workerHeartbeat = (pid) => withJobs((db) => {
1249
+ db.prepare(`UPDATE render_worker SET heartbeat_at = ? WHERE id = 1 AND pid = ?`).run(Date.now(), pid);
1250
+ });
1251
+ var releaseWorkerLock = (pid) => withJobs((db) => {
1252
+ db.prepare(`DELETE FROM render_worker WHERE id = 1 AND pid = ?`).run(pid);
1253
+ });
1254
+ var markOrphanedJobsInterrupted = () => withJobs(
1255
+ (db) => Number(
1256
+ db.prepare(`UPDATE render_jobs SET status = 'interrupted', error = 'The render worker stopped before this job finished.', finished_at = ? WHERE status = 'running'`).run(now()).changes
1257
+ )
1258
+ );
1259
+
1260
+ // src/render/worker.ts
1261
+ import fs5 from "node:fs";
1262
+ import path5 from "node:path";
1263
+ import { spawn as spawn2, spawnSync } from "node:child_process";
1264
+
1265
+ // src/utils/getBinaryPath.ts
1266
+ import {
1267
+ resolveCodeVideoCliRuntime
1268
+ } from "@fullstackcraftllc/codevideo-cli";
1269
+ function getCodeVideoCliRuntime() {
1270
+ return resolveCodeVideoCliRuntime();
609
1271
  }
610
1272
 
611
- const makeVideo = async (actions) => {
1273
+ // src/render/worker.ts
1274
+ var HEARTBEAT_MS = 2e3;
1275
+ var CANCEL_POLL_MS = 500;
1276
+ var KILL_GRACE_MS = 1e4;
1277
+ var log = (message) => {
1278
+ try {
1279
+ fs5.appendFileSync(path5.join(codevideoHome(), "jobs", "worker.log"), `${(/* @__PURE__ */ new Date()).toISOString()} [${process.pid}] ${message}
1280
+ `);
1281
+ } catch {
1282
+ }
1283
+ };
1284
+ var quietly = (what, fn) => {
1285
+ try {
1286
+ fn();
1287
+ } catch (error) {
1288
+ log(`${what} failed: ${error.message}`);
1289
+ }
1290
+ };
1291
+ var ensureWorker = () => {
1292
+ if (liveWorker()) return;
1293
+ const orphaned = markOrphanedJobsInterrupted();
1294
+ if (orphaned > 0) log(`marked ${orphaned} job(s) left by a stopped worker as interrupted`);
1295
+ const entry = process.argv[1];
1296
+ if (!entry) throw new Error("Can't start the render worker: the MCP's entry script is unknown.");
1297
+ const child = spawn2(process.execPath, [...process.execArgv, entry, "--render-worker"], {
1298
+ detached: true,
1299
+ stdio: "ignore",
1300
+ windowsHide: true,
1301
+ env: process.env
1302
+ });
1303
+ child.unref();
1304
+ };
1305
+ var runWorker = async () => {
1306
+ fs5.mkdirSync(path5.join(codevideoHome(), "jobs"), { recursive: true });
1307
+ while (acquireWorkerLock(process.pid)) {
1308
+ log("worker started");
1309
+ const heartbeat = setInterval(() => quietly("heartbeat", () => workerHeartbeat(process.pid)), HEARTBEAT_MS);
612
1310
  try {
613
- const binaryPath = getBinaryPath(); // Use bundled binary instead of PATH_TO_CODEVIDEO_CLI
614
- // Use provided actions or retrieve from storage
615
- const actionsToUse = actions || getCurrentActionsOrThrow();
616
- // Create JSON string directly
617
- const actionsJson = JSON.stringify(actionsToUse);
618
- // Create output file on desktop with timestamp
619
- const homeDir = os.homedir();
620
- const desktopDir = path.join(homeDir, 'Desktop');
621
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
622
- const outputFile = path.join(desktopDir, `codevideo-${timestamp}.mp4`);
623
- // Fire-and-forget: Start the video generation process in the background
624
- const child = spawn(binaryPath, ['make-video'], {
625
- stdio: ['pipe', 'pipe', 'pipe'],
626
- cwd: process.cwd()
627
- });
628
- // Unreference the child process, allowing the parent to exit independently
629
- child.unref();
630
- // Return immediately with status message
631
- return {
632
- outputPath: outputFile,
633
- stdout: `Video generation job with ${actionsToUse.length} actions started in the background via the codevideo-cli command. MP4 file path: ${outputFile}. It should open automatically as soon as it is done being rendered.`,
634
- stderr: ''
635
- };
636
- }
637
- catch (error) {
638
- console.error('Error starting video generation:', error);
639
- return {
640
- outputPath: '',
641
- stdout: '',
642
- stderr: `Failed to start video generation: ${error.message}`,
643
- error
644
- };
1311
+ for (let job = claimNextJob(); job; job = claimNextJob()) {
1312
+ await runJob(job);
1313
+ }
1314
+ } finally {
1315
+ clearInterval(heartbeat);
1316
+ releaseWorkerLock(process.pid);
645
1317
  }
1318
+ if (!hasQueuedJobs()) break;
1319
+ }
1320
+ log("worker exiting: queue empty");
646
1321
  };
647
-
648
- /**
649
- * Validates the actions so the LLM can correct any mistakes it makes
650
- *
651
- */
652
- const validateActions = (actions) => {
653
- // Use provided actions or retrieve from storage
654
- const actionsToUse = actions || getCurrentActionsOrThrow();
655
- // loop over all actions and call the isValidAction function from the types package
656
- const errorMessages = [];
657
- for (var i = 0; i < actionsToUse.length; i++) {
658
- const action = actionsToUse[i];
659
- // codevideo-types package should cover all action types - normal and repeatable!
660
- if (!isValidAction(action)) {
661
- errorMessages.push(`Invalid action at action index ${i}: ${JSON.stringify(action)}. Hint: if this action 'name' is a repeatable action, the 'value' property must be a number. To repeat once, simply use the string "1".`);
662
- }
1322
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1323
+ var groupAlive = (pgid) => {
1324
+ try {
1325
+ process.kill(-pgid, 0);
1326
+ return true;
1327
+ } catch (error) {
1328
+ return error.code === "EPERM";
1329
+ }
1330
+ };
1331
+ var stopTree = async (child) => {
1332
+ if (!child.pid) return;
1333
+ if (process.platform === "win32") {
1334
+ spawnSync("taskkill", ["/PID", String(child.pid), "/T", "/F"], { windowsHide: true });
1335
+ return;
1336
+ }
1337
+ const group = child.pid;
1338
+ const signal = (name) => {
1339
+ try {
1340
+ process.kill(-group, name);
1341
+ } catch {
663
1342
  }
664
- if (errorMessages.length > 0) {
665
- return JSON.stringify({ isError: true, errorMessages });
1343
+ };
1344
+ signal("SIGINT");
1345
+ for (const deadline = Date.now() + KILL_GRACE_MS; groupAlive(group) && Date.now() < deadline; ) await sleep(200);
1346
+ if (groupAlive(group)) {
1347
+ log(`process group ${group} outlived SIGINT by ${KILL_GRACE_MS / 1e3}s; killing it`);
1348
+ signal("SIGKILL");
1349
+ }
1350
+ };
1351
+ var followLines = (stream, file, onLine) => {
1352
+ let pending = "";
1353
+ stream.on("data", (chunk) => {
1354
+ file.write(chunk);
1355
+ pending += chunk.toString("utf8");
1356
+ const lines = pending.split("\n");
1357
+ pending = lines.pop() ?? "";
1358
+ lines.forEach((line) => onLine(line.replace(/\r$/, "")));
1359
+ });
1360
+ stream.on("end", () => {
1361
+ if (pending) onLine(pending);
1362
+ file.end();
1363
+ });
1364
+ };
1365
+ var runJob = async (job) => {
1366
+ const dir = jobDir(job.id);
1367
+ try {
1368
+ const runtime = getCodeVideoCliRuntime();
1369
+ const configPath = path5.join(dir, "config.json");
1370
+ const args = cliArgs({
1371
+ projectJson: fs5.readFileSync(path5.join(dir, "project.json"), "utf8"),
1372
+ outputPath: job.outputPath,
1373
+ configPath: fs5.existsSync(configPath) ? configPath : void 0,
1374
+ options: job.options,
1375
+ open: job.open
1376
+ });
1377
+ fs5.mkdirSync(path5.dirname(job.outputPath), { recursive: true });
1378
+ log(`job ${job.id} (${job.label}) starting`);
1379
+ const [command, commandArgs] = /\.[cm]?js$/i.test(runtime.binaryPath) ? [process.execPath, [runtime.binaryPath, ...args]] : [runtime.binaryPath, args];
1380
+ const child = spawn2(command, commandArgs, {
1381
+ cwd: dir,
1382
+ env: cliEnv(runtime.env, job.options),
1383
+ // Its own process group on Unix, so a cancel can stop Chrome too.
1384
+ detached: process.platform !== "win32",
1385
+ windowsHide: true,
1386
+ stdio: ["ignore", "pipe", "pipe"]
1387
+ });
1388
+ if (child.pid) setJobPid(job.id, child.pid);
1389
+ let lastError = null;
1390
+ let lastProgress = null;
1391
+ const onLine = (line) => {
1392
+ const parsed = parseCliLine(line);
1393
+ if (parsed.kind === "progress" && parsed.text !== lastProgress) {
1394
+ lastProgress = parsed.text;
1395
+ quietly("progress update", () => setJobProgress(job.id, parsed.text));
1396
+ } else if (parsed.kind === "error") {
1397
+ lastError = parsed.text;
1398
+ }
1399
+ };
1400
+ followLines(child.stdout, fs5.createWriteStream(path5.join(dir, "stdout.log")), onLine);
1401
+ followLines(child.stderr, fs5.createWriteStream(path5.join(dir, "stderr.log")), onLine);
1402
+ let stopping;
1403
+ const cancelPoll = setInterval(() => quietly("cancel check", () => {
1404
+ if (!stopping && isCancelRequested(job.id)) {
1405
+ log(`job ${job.id} cancelling`);
1406
+ stopping = stopTree(child);
1407
+ }
1408
+ }), CANCEL_POLL_MS);
1409
+ const exit = await new Promise((resolve) => {
1410
+ child.once("error", (spawnError) => resolve({ code: null, signal: null, spawnError }));
1411
+ child.once("close", (code, signal) => resolve({ code, signal }));
1412
+ });
1413
+ clearInterval(cancelPoll);
1414
+ if (stopping) {
1415
+ await stopping;
1416
+ fs5.rmSync(job.outputPath, { force: true });
1417
+ finishJob(job.id, "cancelled", null);
1418
+ } else if (exit.spawnError) {
1419
+ finishJob(job.id, "failed", `Couldn't start codevideo-cli: ${exit.spawnError.message}`);
1420
+ } else if (exit.code === 0 && fs5.existsSync(job.outputPath) && fs5.statSync(job.outputPath).size > 0) {
1421
+ finishJob(job.id, "succeeded", null);
1422
+ } else {
1423
+ const how = exit.code === 0 ? "exited without writing the video" : exit.code !== null ? `exited with code ${exit.code}` : `was stopped by ${exit.signal}`;
1424
+ finishJob(job.id, "failed", lastError ?? `codevideo-cli ${how}; see ${path5.join(dir, "stderr.log")}`);
666
1425
  }
667
- return JSON.stringify({ isError: false, errorMessages: [] });
1426
+ log(`job ${job.id} finished`);
1427
+ } catch (error) {
1428
+ finishJob(job.id, "failed", error.message);
1429
+ log(`job ${job.id} failed in the worker: ${error.stack ?? error}`);
1430
+ }
668
1431
  };
669
1432
 
670
- /**
671
- * Returns all action names exported by the codevideo-types package, joined by commas.
672
- */
673
- const getActionNames = () => {
674
- return AllActionStrings.join(", ");
1433
+ // src/render/queue.ts
1434
+ var outputDirectory = () => process.env.CODEVIDEO_OUTPUT_DIR || path6.join(os3.homedir(), "Desktop");
1435
+ var timestamp = () => (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1436
+ var newVideoPath = (name) => {
1437
+ const safe = (name ?? "").replace(/[^a-z0-9-_]+/gi, "_").slice(0, 40);
1438
+ return path6.join(outputDirectory(), safe ? `codevideo-${safe}-${timestamp()}.mp4` : `codevideo-${timestamp()}.mp4`);
1439
+ };
1440
+ var estimatedSeconds = (actions, options) => Math.round(estimateVideoDurationInSeconds(actions, estimatePauses(options)));
1441
+ var queueRenders = (specs) => {
1442
+ const jobs = enqueueJobs(specs);
1443
+ ensureWorker();
1444
+ return jobs;
1445
+ };
1446
+ var lastLines = (file, count) => {
1447
+ try {
1448
+ return fs6.readFileSync(file, "utf8").split("\n").map((line) => line.trimEnd()).filter(Boolean).slice(-count);
1449
+ } catch {
1450
+ return [];
1451
+ }
1452
+ };
1453
+ var describeJob = (job, withLog = false) => {
1454
+ const position = job.status === "queued" ? queuePosition(job.id) : null;
1455
+ return {
1456
+ jobId: job.id,
1457
+ label: job.label,
1458
+ status: job.status,
1459
+ ...position !== null ? { jobsAhead: position } : {},
1460
+ outputPath: job.outputPath,
1461
+ ...job.status === "succeeded" ? { saved: true } : {},
1462
+ ...job.progress ? { progress: job.progress } : {},
1463
+ ...job.error ? { error: job.error } : {},
1464
+ createdAt: job.createdAt,
1465
+ ...job.startedAt ? { startedAt: job.startedAt } : {},
1466
+ ...job.finishedAt ? { finishedAt: job.finishedAt } : {},
1467
+ ...withLog ? { logTail: lastLines(path6.join(jobDir(job.id), "stderr.log"), 8), logs: jobDir(job.id) } : {}
1468
+ };
1469
+ };
1470
+ var describeJobById = (jobId, withLog = false) => {
1471
+ const job = getJob(jobId);
1472
+ return job ? describeJob(job, withLog) : null;
675
1473
  };
676
1474
 
677
- const keywordsToExampleMap = [
678
- {
679
- keywords: ["console log", "javascript", "js", "log"],
680
- actions: [
681
- {
682
- name: "author-speak-before",
683
- value: "Today we're going to learn about console.log in JavaScript. Console.log is a function that allows you to print messages to the console, which is useful for debugging and understanding your code better.",
684
- },
685
- {
686
- name: "author-speak-before",
687
- value: "Let's first create an index.js file where we will write our code.",
688
- },
689
- {
690
- name: "mouse-move-file-explorer",
691
- value: "1",
692
- },
693
- {
694
- name: "mouse-right-click",
695
- value: "1",
696
- },
697
- {
698
- name: "mouse-move-file-explorer-context-menu-new-file",
699
- value: "1",
700
- },
701
- {
702
- name: "mouse-left-click",
703
- value: "1",
704
- }
705
- ]
1475
+ // src/tools/make_video_from_actions.ts
1476
+ var queueVideoFromActions = (actions, options = {}, open = true) => {
1477
+ const actionsToUse = actions || getCurrentActionsOrThrow();
1478
+ const [job] = queueRenders([{
1479
+ kind: "actions",
1480
+ label: `${actionsToUse.length} actions`,
1481
+ project: actionsToUse,
1482
+ outputPath: newVideoPath(),
1483
+ options,
1484
+ open,
1485
+ defaultLanguage: options.defaultLanguage ?? "typescript"
1486
+ }]);
1487
+ return { job, estimatedSeconds: estimatedSeconds(actionsToUse, options) };
1488
+ };
1489
+
1490
+ // src/tools/make_video_from_lesson.ts
1491
+ var queueVideoFromLesson = (lesson, options = {}, open = true) => {
1492
+ const lessonToUse = lesson || getCurrentLessonOrThrow();
1493
+ const [job] = queueRenders([{
1494
+ kind: "lesson",
1495
+ label: lessonToUse.name || "lesson",
1496
+ project: lessonToUse,
1497
+ outputPath: newVideoPath(lessonToUse.name),
1498
+ options,
1499
+ open,
1500
+ defaultLanguage: options.defaultLanguage ?? "typescript"
1501
+ }]);
1502
+ return { job, estimatedSeconds: estimatedSeconds(lessonToUse.actions, options) };
1503
+ };
1504
+
1505
+ // src/tools/validate_actions.ts
1506
+ import { isValidAction } from "@fullstackcraftllc/codevideo-types";
1507
+
1508
+ // src/validation/rejectedActions.ts
1509
+ import { AllActionDescriptions as AllActionDescriptions2 } from "@fullstackcraftllc/codevideo-types";
1510
+ var REJECTED_ACTIONS = /* @__PURE__ */ new Map([
1511
+ ["file-explorer-create-file", "make the file with the mouse (right-click, New File) or touch in the terminal"],
1512
+ ["file-explorer-create-folder", "make the folder with the mouse (right-click, New Folder) or mkdir in the terminal"],
1513
+ ["file-explorer-set-file-contents", "type the contents with editor-type"],
1514
+ ["editor-highlight-code", "not supported yet; to point at code, bring its file forward with mouse-move-editor-tab and talk about it"],
1515
+ ["composite-create-file-with-mouse", "not supported yet; make the file with the mouse steps (right-click, New File)"],
1516
+ ["composite-create-file-with-terminal", "not supported yet; type touch <path> with terminal-type, then terminal-enter"],
1517
+ ["composite-create-folder-with-mouse", "not supported yet; make the folder with the mouse steps (right-click, New Folder)"],
1518
+ ["composite-create-folder-with-terminal", "not supported yet; type mkdir <path> with terminal-type, then terminal-enter"],
1519
+ ["composite-close-all-editors-with-mouse", "not supported yet; close editors one at a time with mouse-move-editor-tab-close and mouse-left-click"],
1520
+ ["composite-close-all-editors-with-keyboard", "not supported yet; close editors one at a time with mouse-move-editor-tab-close and mouse-left-click"]
1521
+ ]);
1522
+ var rejectedActionsIn = (actions) => actions.flatMap((action, index) => REJECTED_ACTIONS.has(action.name) ? [{ index, name: action.name }] : []);
1523
+ var describeRejected = (rejected) => rejected.map((action) => `action ${action.index} is ${action.name}: ${REJECTED_ACTIONS.get(action.name)}`).join("; ");
1524
+ var valueHint = (name) => AllActionDescriptions2.find((action) => action.name === name)?.valueDescription;
1525
+
1526
+ // src/tools/validate_actions.ts
1527
+ var validateActions = (actions) => {
1528
+ const actionsToUse = actions || getCurrentActionsOrThrow();
1529
+ const errorMessages = [];
1530
+ for (var i = 0; i < actionsToUse.length; i++) {
1531
+ const action = actionsToUse[i];
1532
+ const hint = valueHint(action.name);
1533
+ if (!isValidAction(action)) {
1534
+ errorMessages.push(hint ? `Invalid action at action index ${i}: ${JSON.stringify(action)}. Its value should be: ${hint}` : `Invalid action at action index ${i}: ${JSON.stringify(action)}. Unknown action name; codevideo_get_action_names lists them all.`);
1535
+ }
1536
+ }
1537
+ const rejected = rejectedActionsIn(actionsToUse);
1538
+ if (rejected.length > 0) {
1539
+ errorMessages.push(`Actions lessons can't use: ${describeRejected(rejected)}.`);
1540
+ }
1541
+ if (errorMessages.length > 0) {
1542
+ return JSON.stringify({ isError: true, errorMessages });
1543
+ }
1544
+ return JSON.stringify({ isError: false, errorMessages: [] });
1545
+ };
1546
+
1547
+ // src/tools/courseTools.ts
1548
+ import fs7 from "node:fs";
1549
+ import path7 from "node:path";
1550
+ import { isValidAction as isValidAction2 } from "@fullstackcraftllc/codevideo-types";
1551
+
1552
+ // src/store/courseStore.ts
1553
+ import { randomUUID as randomUUID2 } from "node:crypto";
1554
+ import { VirtualIDE as VirtualIDE2 } from "@fullstackcraftllc/codevideo-virtual-ide";
1555
+
1556
+ // src/utils/emptyCourseSnapshot.ts
1557
+ var emptyCourseSnapshot = () => ({
1558
+ isUnsavedChangesDialogOpen: false,
1559
+ unsavedFileName: "",
1560
+ fileExplorerSnapshot: {
1561
+ isFileExplorerContextMenuOpen: false,
1562
+ isFileContextMenuOpen: false,
1563
+ isFolderContextMenuOpen: false,
1564
+ isNewFileInputVisible: false,
1565
+ isNewFolderInputVisible: false,
1566
+ isRenameFileInputVisible: false,
1567
+ isRenameFolderInputVisible: false,
1568
+ newFileInputValue: "",
1569
+ newFolderInputValue: "",
1570
+ renameFileInputValue: "",
1571
+ renameFolderInputValue: "",
1572
+ originalFileBeingRenamed: "",
1573
+ originalFolderBeingRenamed: "",
1574
+ newFileParentPath: "",
1575
+ newFolderParentPath: "",
1576
+ fileStructure: {}
1577
+ },
1578
+ editorSnapshot: {
1579
+ isEditorContextMenuOpen: false,
1580
+ editors: []
1581
+ },
1582
+ terminalSnapshot: {
1583
+ terminals: []
1584
+ },
1585
+ mouseSnapshot: {
1586
+ location: "editor",
1587
+ currentHoveredFileName: "",
1588
+ currentHoveredFolderName: "",
1589
+ currentHoveredEditorTabFileName: "",
1590
+ x: 0,
1591
+ y: 0,
1592
+ timestamp: 0,
1593
+ type: "move",
1594
+ buttonStates: {
1595
+ left: false,
1596
+ right: false,
1597
+ middle: false
706
1598
  },
707
- {
708
- keywords: ["golang", "go", "golang console log", "golang log"],
709
- actions: [
710
- {
711
- "name": "author-speak-before",
712
- "value": "Let's learn how to use the fmt.Println function in Go!"
713
- },
714
- {
715
- "name": "author-speak-before",
716
- "value": "Let's first make a main.go file."
717
- },
718
- {
719
- "name": "mouse-move-file-explorer",
720
- "value": "1"
721
- },
722
- {
723
- "name": "mouse-right-click",
724
- "value": "1"
725
- },
726
- {
727
- "name": "mouse-move-file-explorer-context-menu-new-file",
728
- "value": "1"
729
- },
730
- {
731
- "name": "mouse-left-click",
732
- "value": "1"
733
- },
734
- {
735
- "name": "file-explorer-type-new-file-input",
736
- "value": "main.go"
737
- },
738
- {
739
- "name": "file-explorer-enter-new-file-input",
740
- "value": "1"
741
- },
742
- {
743
- "name": "author-speak-before",
744
- "value": "First, we'll need to declare the main package, and then the main function."
745
- },
746
- {
747
- "name": "editor-type",
748
- "value": "package main"
749
- },
750
- {
751
- "name": "editor-enter",
752
- "value": "2"
753
- },
754
- {
755
- "name": "author-speak-before",
756
- "value": "And we'll need the fmt package to use the Println function."
757
- },
758
- {
759
- "name": "editor-type",
760
- "value": 'import "fmt"'
761
- },
762
- {
763
- "name": "editor-enter",
764
- "value": "1"
765
- },
766
- {
767
- "name": "editor-type",
768
- "value": "func main() {"
769
- },
770
- {
771
- "name": "editor-enter",
772
- "value": "1"
773
- },
774
- {
775
- "name": "author-speak-before",
776
- "value": "and let's just print 'Hello world!' to the console."
777
- },
778
- {
779
- "name": "editor-type",
780
- "value": ' fmt.Println("Hello, world!")'
781
- },
782
- {
783
- "name": "editor-enter",
784
- "value": "1"
785
- },
786
- {
787
- "name": "editor-type",
788
- "value": "}"
789
- },
790
- {
791
- "name": "editor-arrow-up",
792
- "value": "1"
793
- },
794
- {
795
- "name": "editor-command-right",
796
- "value": "1"
797
- },
798
- {
799
- "name": "author-speak-before",
800
- "value": "and if I wanted to write the value of some variable to the console, I could do that like so:"
801
- },
802
- {
803
- "name": "editor-backspace",
804
- "value": "32"
805
- },
806
- {
807
- "name": "editor-type",
808
- "value": " myVariable := 5"
809
- },
810
- {
811
- "name": "editor-enter",
812
- "value": "1"
813
- },
814
- {
815
- "name": "editor-type",
816
- "value": " fmt.Println(myVariable)"
817
- },
818
- {
819
- "name": "author-speak-before",
820
- "value": "Now, when I run this code, I would expect the value of 'myVariable' to be printed to the console. Something like:"
821
- },
822
- {
823
- "name": "editor-enter",
824
- "value": "1"
825
- },
826
- {
827
- "name": "editor-type",
828
- "value": " // 5"
829
- },
830
- {
831
- "name": "author-speak-before",
832
- "value": "Console logging is simple, yet powerful and very useful!"
1599
+ scrollPosition: {
1600
+ x: 0,
1601
+ y: 0
1602
+ }
1603
+ },
1604
+ authorSnapshot: {
1605
+ authors: [
1606
+ { currentSpeechCaption: "" }
1607
+ ]
1608
+ }
1609
+ });
1610
+
1611
+ // src/store/courseStore.ts
1612
+ var COURSE_SCHEMA = `
1613
+ CREATE TABLE IF NOT EXISTS courses (
1614
+ id TEXT PRIMARY KEY,
1615
+ name TEXT NOT NULL,
1616
+ description TEXT NOT NULL DEFAULT '',
1617
+ primary_language TEXT NOT NULL DEFAULT '',
1618
+ lesson_order TEXT NOT NULL DEFAULT '[]',
1619
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
1620
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
1621
+ );
1622
+ CREATE TABLE IF NOT EXISTS lessons (
1623
+ id TEXT PRIMARY KEY,
1624
+ course_id TEXT NOT NULL,
1625
+ name TEXT NOT NULL,
1626
+ description TEXT NOT NULL DEFAULT '',
1627
+ must_compile INTEGER NOT NULL DEFAULT 1,
1628
+ continues_from_previous INTEGER NOT NULL DEFAULT 1,
1629
+ actions TEXT NOT NULL DEFAULT '[]',
1630
+ initial_snapshot TEXT NOT NULL,
1631
+ final_snapshot TEXT NOT NULL,
1632
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
1633
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
1634
+ );
1635
+ CREATE INDEX IF NOT EXISTS idx_lessons_course ON lessons(course_id);
1636
+ `;
1637
+ var withDb = (fn) => withDatabase((db) => {
1638
+ db.exec(COURSE_SCHEMA);
1639
+ return fn(db);
1640
+ });
1641
+ var asRow = (row) => row;
1642
+ var replay = (initial, actions) => {
1643
+ const virtualIDE = new VirtualIDE2();
1644
+ virtualIDE.applyCourseSnapshot(initial);
1645
+ if (actions.length > 0) {
1646
+ virtualIDE.applyActions(actions);
1647
+ }
1648
+ return virtualIDE.getCourseSnapshot();
1649
+ };
1650
+ var replayBreaks = (initial, actions) => {
1651
+ try {
1652
+ replay(initial, actions);
1653
+ return false;
1654
+ } catch {
1655
+ return true;
1656
+ }
1657
+ };
1658
+ var replayError = (initial, actions, error) => {
1659
+ if (replayBreaks(initial, [])) return new Error(`The lesson's starting state can't be loaded: ${error.message}`);
1660
+ let works = 0;
1661
+ let breaks = actions.length;
1662
+ while (breaks - works > 1) {
1663
+ const mid = works + breaks >> 1;
1664
+ if (replayBreaks(initial, actions.slice(0, mid))) breaks = mid;
1665
+ else works = mid;
1666
+ }
1667
+ const index = breaks - 1;
1668
+ const action = actions[index];
1669
+ const pathHint = `names something that exists at that point, by path (e.g. "src/index.ts")`;
1670
+ const moveIndex = /click/.test(action.name) ? actions.slice(0, index).map((a) => a.name.startsWith("mouse-move")).lastIndexOf(true) : -1;
1671
+ if (moveIndex >= 0) {
1672
+ const move = actions[moveIndex];
1673
+ return new Error(
1674
+ `Action ${index} (${action.name}) clicks what action ${moveIndex} (${move.name} ${JSON.stringify(move.value)}) points at, and that breaks the replay: ${error.message}. Check that action ${moveIndex}'s value ${pathHint}.`
1675
+ );
1676
+ }
1677
+ return new Error(`Action ${index} (${action.name} ${JSON.stringify(action.value)}) breaks the replay: ${error.message}. Check that its value ${pathHint}.`);
1678
+ };
1679
+ var snapshotFromActions = (initial, actions) => {
1680
+ try {
1681
+ return replay(initial, actions);
1682
+ } catch (error) {
1683
+ throw replayError(initial, actions, error);
1684
+ }
1685
+ };
1686
+ var fileEntriesFromSnapshot = (snapshot) => {
1687
+ const virtualIDE = new VirtualIDE2();
1688
+ virtualIDE.applyCourseSnapshot(snapshot);
1689
+ return virtualIDE.getFullFilePathsAndContents();
1690
+ };
1691
+ var rowToLesson = (row) => ({
1692
+ id: row.id,
1693
+ courseId: row.course_id,
1694
+ name: row.name,
1695
+ description: row.description,
1696
+ mustCompile: row.must_compile === 1,
1697
+ continuesFromPrevious: row.continues_from_previous === 1,
1698
+ actions: JSON.parse(row.actions),
1699
+ initialSnapshot: JSON.parse(row.initial_snapshot),
1700
+ finalSnapshot: JSON.parse(row.final_snapshot)
1701
+ });
1702
+ var getLessonOrder = (db, courseId) => {
1703
+ const row = asRow(db.prepare(`SELECT lesson_order FROM courses WHERE id = ?`).get(courseId));
1704
+ if (!row) throw new Error(`Course not found: ${courseId}`);
1705
+ return JSON.parse(row.lesson_order);
1706
+ };
1707
+ var getLessonsOrdered = (db, courseId) => {
1708
+ const order = getLessonOrder(db, courseId);
1709
+ const get = db.prepare(`SELECT * FROM lessons WHERE id = ?`);
1710
+ return order.map((id) => asRow(get.get(id))).filter((r) => !!r).map(rowToLesson);
1711
+ };
1712
+ var recomputeChain = (db, courseId) => {
1713
+ const order = getLessonOrder(db, courseId);
1714
+ const getRow = db.prepare(`SELECT * FROM lessons WHERE id = ?`);
1715
+ const update = db.prepare(`UPDATE lessons SET initial_snapshot = ?, final_snapshot = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`);
1716
+ let previousFinal = null;
1717
+ for (const lessonId of order) {
1718
+ const row = asRow(getRow.get(lessonId));
1719
+ if (!row) continue;
1720
+ const lesson = rowToLesson(row);
1721
+ const initial = lesson.continuesFromPrevious && previousFinal ? previousFinal : lesson.initialSnapshot;
1722
+ let final;
1723
+ try {
1724
+ final = snapshotFromActions(initial, lesson.actions);
1725
+ } catch (error) {
1726
+ throw new Error(`Lesson "${lesson.name}": ${error.message}`);
1727
+ }
1728
+ update.run(JSON.stringify(initial), JSON.stringify(final), lessonId);
1729
+ previousFinal = final;
1730
+ }
1731
+ };
1732
+ var createCourse = (input) => withDb((db) => {
1733
+ const id = input.id && input.id.trim().length > 0 ? input.id.trim() : randomUUID2();
1734
+ db.prepare(`
1735
+ INSERT INTO courses (id, name, description, primary_language, lesson_order)
1736
+ VALUES (?, ?, ?, ?, '[]')
1737
+ `).run(id, input.name, input.description ?? "", input.primaryLanguage ?? "");
1738
+ return id;
1739
+ });
1740
+ var addLesson = (courseId, input) => withDb((db) => {
1741
+ const order = getLessonOrder(db, courseId);
1742
+ const continuesFromPrevious = input.continuesFromPrevious ?? order.length > 0;
1743
+ let initial;
1744
+ if (continuesFromPrevious && order.length > 0) {
1745
+ const prev = asRow(db.prepare(`SELECT final_snapshot FROM lessons WHERE id = ?`).get(order[order.length - 1]));
1746
+ initial = prev ? JSON.parse(prev.final_snapshot) : emptyCourseSnapshot();
1747
+ } else {
1748
+ initial = input.initialSnapshot ?? emptyCourseSnapshot();
1749
+ }
1750
+ const lessonId = randomUUID2();
1751
+ transaction(db, () => {
1752
+ db.prepare(`
1753
+ INSERT INTO lessons (id, course_id, name, description, must_compile, continues_from_previous, actions, initial_snapshot, final_snapshot)
1754
+ VALUES (?, ?, ?, ?, ?, ?, '[]', ?, ?)
1755
+ `).run(
1756
+ lessonId,
1757
+ courseId,
1758
+ input.name,
1759
+ input.description ?? "",
1760
+ input.mustCompile ?? true ? 1 : 0,
1761
+ continuesFromPrevious ? 1 : 0,
1762
+ JSON.stringify(initial),
1763
+ JSON.stringify(initial)
1764
+ // no actions yet => final == initial
1765
+ );
1766
+ order.push(lessonId);
1767
+ db.prepare(`UPDATE courses SET lesson_order = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`).run(JSON.stringify(order), courseId);
1768
+ });
1769
+ return lessonId;
1770
+ });
1771
+ var setLessonActions = (courseId, lessonId, actions) => withDb(
1772
+ (db) => transaction(db, () => {
1773
+ const exists = db.prepare(`SELECT 1 FROM lessons WHERE id = ? AND course_id = ?`).get(lessonId, courseId);
1774
+ if (!exists) throw new Error(`Lesson not found: ${lessonId} in course ${courseId}`);
1775
+ db.prepare(`UPDATE lessons SET actions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`).run(JSON.stringify(actions), lessonId);
1776
+ recomputeChain(db, courseId);
1777
+ return { actionCount: actions.length };
1778
+ })
1779
+ );
1780
+ var editLessonActions = (courseId, lessonId, edits) => withDb(
1781
+ (db) => transaction(db, () => {
1782
+ const row = asRow(db.prepare(`SELECT actions FROM lessons WHERE id = ? AND course_id = ?`).get(lessonId, courseId));
1783
+ if (!row) throw new Error(`Lesson not found: ${lessonId} in course ${courseId}`);
1784
+ const actions = JSON.parse(row.actions);
1785
+ edits.forEach((edit, i) => {
1786
+ if (!Number.isInteger(edit.start) || edit.start < 0 || edit.start > actions.length) {
1787
+ throw new Error(`Edit ${i}: start ${edit.start} is outside 0..${actions.length}`);
1788
+ }
1789
+ if (!Number.isInteger(edit.deleteCount) || edit.deleteCount < 0 || edit.start + edit.deleteCount > actions.length) {
1790
+ throw new Error(`Edit ${i}: deleting ${edit.deleteCount} from ${edit.start} runs past the last action (${actions.length})`);
1791
+ }
1792
+ actions.splice(edit.start, edit.deleteCount, ...edit.insert);
1793
+ });
1794
+ db.prepare(`UPDATE lessons SET actions = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`).run(JSON.stringify(actions), lessonId);
1795
+ recomputeChain(db, courseId);
1796
+ return { actionCount: actions.length };
1797
+ })
1798
+ );
1799
+ var getLessonActions = (courseId, lessonId, start = 0, limit = 50) => {
1800
+ const lesson = getLesson(courseId, lessonId);
1801
+ const from = Math.max(0, Math.min(start, lesson.actions.length));
1802
+ return {
1803
+ total: lesson.actions.length,
1804
+ start: from,
1805
+ actions: lesson.actions.slice(from, from + Math.max(0, limit)).map((a, i) => ({ index: from + i, name: a.name, value: a.value }))
1806
+ };
1807
+ };
1808
+ var getLesson = (courseId, lessonId) => withDb((db) => {
1809
+ const row = asRow(db.prepare(`SELECT * FROM lessons WHERE id = ? AND course_id = ?`).get(lessonId, courseId));
1810
+ if (!row) throw new Error(`Lesson not found: ${lessonId} in course ${courseId}`);
1811
+ return rowToLesson(row);
1812
+ });
1813
+ var listLessons = (courseId) => withDb((db) => getLessonsOrdered(db, courseId));
1814
+ var getCourse = (courseId) => withDb((db) => {
1815
+ const c = asRow(
1816
+ db.prepare(`SELECT id, name, description, primary_language FROM courses WHERE id = ?`).get(courseId)
1817
+ );
1818
+ if (!c) throw new Error(`Course not found: ${courseId}`);
1819
+ return {
1820
+ id: c.id,
1821
+ name: c.name,
1822
+ description: c.description,
1823
+ primaryLanguage: c.primary_language,
1824
+ lessons: getLessonsOrdered(db, courseId)
1825
+ };
1826
+ });
1827
+ var exportCourse = (courseId) => {
1828
+ const course = getCourse(courseId);
1829
+ const lessons = course.lessons.map((l) => ({
1830
+ id: l.id,
1831
+ name: l.name,
1832
+ description: l.description,
1833
+ actions: l.actions,
1834
+ initialSnapshot: l.initialSnapshot,
1835
+ finalSnapshot: l.finalSnapshot
1836
+ }));
1837
+ return {
1838
+ id: course.id,
1839
+ name: course.name,
1840
+ description: course.description,
1841
+ primaryLanguage: course.primaryLanguage,
1842
+ lessons
1843
+ };
1844
+ };
1845
+ var getLessonFileTree = (courseId, lessonId) => {
1846
+ const lesson = getLesson(courseId, lessonId);
1847
+ return fileEntriesFromSnapshot(lesson.finalSnapshot).map((f) => f.path).sort();
1848
+ };
1849
+ var samePath = (a, b) => a.replace(/^\.?\/+/, "") === b.replace(/^\.?\/+/, "");
1850
+ var getLessonFile = (courseId, lessonId, filePath) => {
1851
+ const lesson = getLesson(courseId, lessonId);
1852
+ const entry = fileEntriesFromSnapshot(lesson.finalSnapshot).find((f) => samePath(f.path, filePath));
1853
+ return entry ? entry.content : null;
1854
+ };
1855
+ var getLessonFiles = (courseId, lessonId) => {
1856
+ const lesson = getLesson(courseId, lessonId);
1857
+ return fileEntriesFromSnapshot(lesson.finalSnapshot);
1858
+ };
1859
+
1860
+ // src/tools/compileCheck.ts
1861
+ import { createRequire as createRequire2 } from "node:module";
1862
+ var cachedCompileProject = null;
1863
+ var getCompileProject = () => {
1864
+ if (!cachedCompileProject) {
1865
+ const require2 = createRequire2(import.meta.url);
1866
+ cachedCompileProject = require2("@fullstackcraftllc/codevideo-dynamic-ast").compileProject;
1867
+ }
1868
+ return cachedCompileProject;
1869
+ };
1870
+ var compileFiles = async (files) => {
1871
+ if (files.length === 0) {
1872
+ return { status: "ok", projects: [], message: "No files to compile." };
1873
+ }
1874
+ try {
1875
+ const compileProject = getCompileProject();
1876
+ const result = await compileProject(files);
1877
+ const projects = result.projects.map((p) => ({
1878
+ language: p.language,
1879
+ errors: p.errors
1880
+ }));
1881
+ const hasErrors = projects.some((p) => p.errors.length > 0);
1882
+ return { status: hasErrors ? "errors" : "ok", projects };
1883
+ } catch (error) {
1884
+ const message = error instanceof Error ? error.message : String(error);
1885
+ return {
1886
+ status: "errors",
1887
+ projects: [{ language: "unknown", errors: [{ file: "unknown", line: 0, message: `Compile check failed to run: ${message}` }] }]
1888
+ };
1889
+ }
1890
+ };
1891
+
1892
+ // src/tools/courseTools.ts
1893
+ var ok = (data) => JSON.stringify(data, null, 2);
1894
+ var createCourseTool = (input) => {
1895
+ const courseId = createCourse(input);
1896
+ return ok({ courseId, message: `Created course "${input.name}". Add lessons with codevideo_add_lesson(courseId).` });
1897
+ };
1898
+ var filesChangedBy = (lesson) => {
1899
+ const before = new Map(fileEntriesFromSnapshot(lesson.initialSnapshot).map((f) => [f.path, f.content]));
1900
+ const open = new Set((lesson.finalSnapshot.editorSnapshot?.editors ?? []).map((e) => e.filename));
1901
+ return fileEntriesFromSnapshot(lesson.finalSnapshot).filter((f) => before.get(f.path) !== f.content).slice(0, 10).map((f) => {
1902
+ const path9 = f.path.replace(/^\/+/, "");
1903
+ return { path: path9, change: before.has(f.path) ? "changed" : "created", open: open.has(path9) };
1904
+ });
1905
+ };
1906
+ var addLessonTool = (courseId, input) => {
1907
+ const lessonId = addLesson(courseId, input);
1908
+ const lessons = listLessons(courseId);
1909
+ const index = lessons.findIndex((l) => l.id === lessonId);
1910
+ const lesson = lessons[index];
1911
+ const previous = index > 0 ? lessons[index - 1] : void 0;
1912
+ return ok({
1913
+ lessonId,
1914
+ continuesFromPrevious: lesson.continuesFromPrevious,
1915
+ mustCompile: lesson.mustCompile,
1916
+ // What the opening recap is written from, without loading that lesson.
1917
+ ...previous ? { previousLesson: { name: previous.name, description: previous.description, files: filesChangedBy(previous) } } : {},
1918
+ message: `Added lesson "${input.name}". ` + (lesson.continuesFromPrevious ? "Its initial state is the previous lesson's final state (chained server-side)." : "Its initial state is empty (from scratch).") + " Author its actions, then call codevideo_set_lesson_actions(courseId, lessonId, actions)."
1919
+ });
1920
+ };
1921
+ var problemsWith = (actions) => {
1922
+ const rejected = rejectedActionsIn(actions);
1923
+ return rejected.length === 0 ? {} : { problems: `Stored, but codevideo_validate_course rejects this lesson until these are replaced: ${describeRejected(rejected)}. codevideo_edit_lesson_actions can swap just those actions.` };
1924
+ };
1925
+ var setLessonActionsTool = (courseId, lessonId, actions) => {
1926
+ const { actionCount } = setLessonActions(courseId, lessonId, actions);
1927
+ return ok({
1928
+ ok: true,
1929
+ actionCount,
1930
+ message: `Stored ${actionCount} actions. Final snapshot recomputed server-side. Use codevideo_compile_lesson to check it builds.`,
1931
+ ...problemsWith(actions)
1932
+ });
1933
+ };
1934
+ var editLessonActionsTool = (courseId, lessonId, edits) => {
1935
+ const { actionCount } = editLessonActions(courseId, lessonId, edits);
1936
+ return ok({
1937
+ ok: true,
1938
+ actionCount,
1939
+ message: `Applied ${edits.length} edit(s); the lesson now has ${actionCount} actions. Snapshots recomputed server-side.`,
1940
+ ...problemsWith(getLesson(courseId, lessonId).actions)
1941
+ });
1942
+ };
1943
+ var getLessonActionsTool = (courseId, lessonId, start, limit) => ok({ courseId, lessonId, ...getLessonActions(courseId, lessonId, start, limit) });
1944
+ var getLessonFileTreeTool = (courseId, lessonId) => ok({ courseId, lessonId, files: getLessonFileTree(courseId, lessonId) });
1945
+ var getLessonFileTool = (courseId, lessonId, filePath) => {
1946
+ const content = getLessonFile(courseId, lessonId, filePath);
1947
+ if (content === null) {
1948
+ return ok({ courseId, lessonId, path: filePath, found: false, message: "No such file at the end of this lesson." });
1949
+ }
1950
+ return ok({ courseId, lessonId, path: filePath, found: true, content });
1951
+ };
1952
+ var compileLessonTool = async (courseId, lessonId) => {
1953
+ const files = getLessonFiles(courseId, lessonId);
1954
+ const result = await compileFiles(files);
1955
+ return ok({ courseId, lessonId, fileCount: files.length, ...result });
1956
+ };
1957
+ var validateCourse = async (courseId) => {
1958
+ const lessons = listLessons(courseId);
1959
+ const results = [];
1960
+ for (let i = 0; i < lessons.length; i++) {
1961
+ const lesson = lessons[i];
1962
+ const invalidActionIndexes = [];
1963
+ lesson.actions.forEach((a, idx) => {
1964
+ if (!isValidAction2(a)) invalidActionIndexes.push(idx);
1965
+ });
1966
+ const rejectedActions = rejectedActionsIn(lesson.actions);
1967
+ const opensWithSpeech = lesson.actions.length === 0 || lesson.actions[0].name === "author-speak-before";
1968
+ let snapshotContinuous = true;
1969
+ if (i > 0 && lesson.continuesFromPrevious) {
1970
+ snapshotContinuous = JSON.stringify(lesson.initialSnapshot) === JSON.stringify(lessons[i - 1].finalSnapshot);
1971
+ }
1972
+ let compileStatus = "skipped";
1973
+ let compileErrors = [];
1974
+ if (lesson.mustCompile) {
1975
+ const c = await compileFiles(getLessonFiles(courseId, lesson.id));
1976
+ compileStatus = c.status;
1977
+ compileErrors = c.projects.filter((p) => p.errors.length > 0);
1978
+ }
1979
+ results.push({
1980
+ lessonId: lesson.id,
1981
+ name: lesson.name,
1982
+ index: i,
1983
+ actionsValid: invalidActionIndexes.length === 0,
1984
+ hasActions: lesson.actions.length > 0,
1985
+ invalidActionIndexes,
1986
+ rejectedActions,
1987
+ opensWithSpeech,
1988
+ snapshotContinuous,
1989
+ mustCompile: lesson.mustCompile,
1990
+ compileStatus,
1991
+ compileErrors
1992
+ });
1993
+ }
1994
+ const allActionsValid = results.every((r) => r.actionsValid);
1995
+ const allAllowed = results.every((r) => r.rejectedActions.length === 0);
1996
+ const allOpenWithSpeech = results.every((r) => r.opensWithSpeech);
1997
+ const allHaveActions = results.every((r) => r.hasActions);
1998
+ const allContinuous = results.every((r) => r.snapshotContinuous);
1999
+ const compileEnforced = results.filter((r) => r.mustCompile);
2000
+ const allMustCompileClean = compileEnforced.every((r) => r.compileStatus === "ok");
2001
+ const finalLesson = results[results.length - 1];
2002
+ const finalCompilesClean = !!finalLesson && finalLesson.mustCompile && finalLesson.compileStatus === "ok";
2003
+ const done = lessons.length > 0 && allHaveActions && allActionsValid && allAllowed && allOpenWithSpeech && allContinuous && allMustCompileClean && finalCompilesClean;
2004
+ const reasons = [];
2005
+ if (lessons.length === 0) reasons.push("Course has no lessons.");
2006
+ for (const lesson of results.filter((result) => !result.hasActions)) {
2007
+ reasons.push(`Lesson "${lesson.name}" (${lesson.lessonId}) has no actions.`);
2008
+ }
2009
+ results.forEach((result, i) => {
2010
+ if (result.invalidActionIndexes.length === 0) return;
2011
+ const details = result.invalidActionIndexes.map((index) => {
2012
+ const action = lessons[i].actions[index];
2013
+ const hint = valueHint(action.name);
2014
+ return hint ? `action ${index} is ${action.name} ${JSON.stringify(action.value)}, but its value should be: ${hint}` : `action ${index} has an unknown name, ${JSON.stringify(action.name)}`;
2015
+ });
2016
+ reasons.push(`Lesson "${result.name}" (${result.lessonId}) has invalid actions: ${details.join("; ")}.`);
2017
+ });
2018
+ for (const result of results.filter((r) => !r.opensWithSpeech)) {
2019
+ reasons.push(`Lesson "${result.name}" (${result.lessonId}) must open with author-speak-before: frame the lesson, and after the first lesson start by recapping the previous one.`);
2020
+ }
2021
+ for (const result of results.filter((r) => r.rejectedActions.length > 0)) {
2022
+ reasons.push(`Lesson "${result.name}" (${result.lessonId}) uses actions lessons can't use; ${describeRejected(result.rejectedActions)}.`);
2023
+ }
2024
+ if (!allContinuous) reasons.push("Some lessons do not follow cleanly from the previous lesson (snapshot discontinuity).");
2025
+ if (!allMustCompileClean) reasons.push("Some lessons marked mustCompile do not compile cleanly.");
2026
+ if (finalLesson && !finalLesson.mustCompile) reasons.push("The final lesson must be marked mustCompile (all code should work by the end).");
2027
+ else if (!finalCompilesClean) reasons.push("The final lesson does not compile cleanly.");
2028
+ return { courseId, done, lessonCount: lessons.length, lessons: results, reasons };
2029
+ };
2030
+ var validateCourseTool = async (courseId) => ok(await validateCourse(courseId));
2031
+ var getCourseStatusTool = (courseId) => {
2032
+ const course = getCourse(courseId);
2033
+ return ok({
2034
+ courseId: course.id,
2035
+ name: course.name,
2036
+ primaryLanguage: course.primaryLanguage,
2037
+ lessonCount: course.lessons.length,
2038
+ lessons: course.lessons.map((l, i) => ({
2039
+ index: i,
2040
+ lessonId: l.id,
2041
+ name: l.name,
2042
+ actionCount: l.actions.length,
2043
+ // At the render page's default pauses, with narration estimated from word counts.
2044
+ estimatedSeconds: estimatedSeconds(l.actions),
2045
+ mustCompile: l.mustCompile,
2046
+ continuesFromPrevious: l.continuesFromPrevious,
2047
+ hasActions: l.actions.length > 0
2048
+ }))
2049
+ });
2050
+ };
2051
+ var exportCourseTool = (courseId) => {
2052
+ const course = exportCourse(courseId);
2053
+ const dir = path7.join(codevideoHome(), "exports");
2054
+ fs7.mkdirSync(dir, { recursive: true });
2055
+ const file = path7.join(dir, `${courseId.replace(/[^a-z0-9-_]+/gi, "_")}.json`);
2056
+ const json = JSON.stringify(course, null, 2);
2057
+ fs7.writeFileSync(file, json);
2058
+ return ok({
2059
+ courseId,
2060
+ path: file,
2061
+ lessonCount: course.lessons.length,
2062
+ bytes: Buffer.byteLength(json),
2063
+ message: "Exported to a file. Open or pass that path instead of reading the course into context."
2064
+ });
2065
+ };
2066
+
2067
+ // src/tools/renderTools.ts
2068
+ import os4 from "os";
2069
+ import fs8 from "fs";
2070
+ import path8 from "path";
2071
+ import { generateHtmlStringFromLesson } from "@fullstackcraftllc/codevideo-exporters";
2072
+ var ok2 = (data) => JSON.stringify(data, null, 2);
2073
+ var toILesson = (l) => ({
2074
+ id: l.id,
2075
+ name: l.name,
2076
+ description: l.description,
2077
+ actions: l.actions,
2078
+ initialSnapshot: l.initialSnapshot,
2079
+ finalSnapshot: l.finalSnapshot
2080
+ });
2081
+ var writePreviewHtml = async (courseId, l) => {
2082
+ const html = await generateHtmlStringFromLesson(toILesson(l), {});
2083
+ const outDir = path8.join(os4.homedir(), ".codevideo", "previews", courseId);
2084
+ fs8.mkdirSync(outDir, { recursive: true });
2085
+ const file = path8.join(outDir, `${l.id}.html`);
2086
+ fs8.writeFileSync(file, html, "utf8");
2087
+ return file;
2088
+ };
2089
+ var generateLessonPreviewTool = async (courseId, lessonId) => {
2090
+ const file = await writePreviewHtml(courseId, getLesson(courseId, lessonId));
2091
+ return ok2({ courseId, lessonId, previewHtml: file, message: "Open this HTML file in a browser to preview the lesson." });
2092
+ };
2093
+ var generateCoursePreviewsTool = async (courseId) => {
2094
+ const validation = await validateCourse(courseId);
2095
+ if (!validation.done) {
2096
+ return ok2({
2097
+ ok: false,
2098
+ gated: true,
2099
+ message: "Course has not passed validation yet \u2014 fix it first, then generate previews.",
2100
+ reasons: validation.reasons
2101
+ });
2102
+ }
2103
+ const previews = [];
2104
+ for (const l of listLessons(courseId)) {
2105
+ previews.push({ lessonId: l.id, name: l.name, previewHtml: await writePreviewHtml(courseId, l) });
2106
+ }
2107
+ return ok2({
2108
+ ok: true,
2109
+ courseId,
2110
+ previews,
2111
+ message: "Previews generated. Review them; once a human approves, call codevideo_render_course_videos to spawn per-lesson video rendering."
2112
+ });
2113
+ };
2114
+ var lessonSpec = (course, l, options, open) => ({
2115
+ kind: "course-lesson",
2116
+ courseId: course.id,
2117
+ lessonId: l.id,
2118
+ label: l.name || "lesson",
2119
+ project: toILesson(l),
2120
+ outputPath: newVideoPath(l.name || "lesson"),
2121
+ options,
2122
+ open,
2123
+ defaultLanguage: options.defaultLanguage ?? (course.primaryLanguage || "typescript")
2124
+ });
2125
+ var renderLessonVideoTool = async (courseId, lessonId, options = {}, open = true) => {
2126
+ const validation = await validateCourse(courseId);
2127
+ if (!validation.done) {
2128
+ return ok2({ ok: false, gated: true, message: "No video until the course passes validation. Fix the course first.", reasons: validation.reasons });
2129
+ }
2130
+ const course = getCourse(courseId);
2131
+ const lesson = getLesson(courseId, lessonId);
2132
+ const [job] = queueRenders([lessonSpec(course, lesson, options, open)]);
2133
+ return ok2({
2134
+ ok: true,
2135
+ ...describeJob(job),
2136
+ estimatedSeconds: estimatedSeconds(lesson.actions, options),
2137
+ message: "Queued. It renders in the background; check it with codevideo_get_render_job."
2138
+ });
2139
+ };
2140
+ var renderCourseVideosTool = async (courseId, options = {}, open = false) => {
2141
+ const validation = await validateCourse(courseId);
2142
+ if (!validation.done) {
2143
+ return ok2({ ok: false, gated: true, message: "No video until the course passes validation. Fix the course first.", reasons: validation.reasons });
2144
+ }
2145
+ const course = getCourse(courseId);
2146
+ const jobs = queueRenders(course.lessons.map((l) => lessonSpec(course, l, options, open)));
2147
+ return ok2({
2148
+ ok: true,
2149
+ courseId,
2150
+ jobs: jobs.map((job, i) => ({
2151
+ jobId: job.id,
2152
+ lessonId: job.lessonId,
2153
+ label: job.label,
2154
+ outputPath: job.outputPath,
2155
+ estimatedSeconds: estimatedSeconds(course.lessons[i].actions, options)
2156
+ })),
2157
+ message: `Queued ${jobs.length} lesson video(s). They render one at a time in the background; check them with codevideo_list_render_jobs({ courseId }).`
2158
+ });
2159
+ };
2160
+
2161
+ // src/resources/get_action_names.ts
2162
+ var getActionNames = () => {
2163
+ return actionReference();
2164
+ };
2165
+
2166
+ // src/resources/get_example_actions_array_by_keyword.ts
2167
+ var keywordsToExampleActionsMap = [
2168
+ {
2169
+ keywords: ["console log", "javascript", "js", "log"],
2170
+ actions: [
2171
+ {
2172
+ name: "author-speak-before",
2173
+ value: "Today we're going to learn about console.log in JavaScript. Console.log is a function that allows you to print messages to the console, which is useful for debugging and understanding your code better."
2174
+ },
2175
+ {
2176
+ name: "author-speak-before",
2177
+ value: "Let's first create an index.js file where we will write our code."
2178
+ },
2179
+ {
2180
+ name: "mouse-move-file-explorer",
2181
+ value: "1"
2182
+ },
2183
+ {
2184
+ name: "mouse-right-click",
2185
+ value: "1"
2186
+ },
2187
+ {
2188
+ name: "mouse-move-file-explorer-context-menu-new-file",
2189
+ value: "1"
2190
+ },
2191
+ {
2192
+ name: "mouse-left-click",
2193
+ value: "1"
2194
+ }
2195
+ ]
2196
+ },
2197
+ {
2198
+ keywords: ["golang", "go", "golang console log", "golang log"],
2199
+ actions: [
2200
+ {
2201
+ "name": "author-speak-before",
2202
+ "value": "Let's learn how to use the fmt.Println function in Go!"
2203
+ },
2204
+ {
2205
+ "name": "author-speak-before",
2206
+ "value": "Let's first make a main.go file."
2207
+ },
2208
+ {
2209
+ "name": "mouse-move-file-explorer",
2210
+ "value": "1"
2211
+ },
2212
+ {
2213
+ "name": "mouse-right-click",
2214
+ "value": "1"
2215
+ },
2216
+ {
2217
+ "name": "mouse-move-file-explorer-context-menu-new-file",
2218
+ "value": "1"
2219
+ },
2220
+ {
2221
+ "name": "mouse-left-click",
2222
+ "value": "1"
2223
+ },
2224
+ {
2225
+ "name": "file-explorer-type-new-file-input",
2226
+ "value": "main.go"
2227
+ },
2228
+ {
2229
+ "name": "file-explorer-enter-new-file-input",
2230
+ "value": "1"
2231
+ },
2232
+ {
2233
+ "name": "author-speak-before",
2234
+ "value": "First, we'll need to declare the main package, and then the main function."
2235
+ },
2236
+ {
2237
+ "name": "editor-type",
2238
+ "value": "package main"
2239
+ },
2240
+ {
2241
+ "name": "editor-enter",
2242
+ "value": "2"
2243
+ },
2244
+ {
2245
+ "name": "author-speak-before",
2246
+ "value": "And we'll need the fmt package to use the Println function."
2247
+ },
2248
+ {
2249
+ "name": "editor-type",
2250
+ "value": 'import "fmt"'
2251
+ },
2252
+ {
2253
+ "name": "editor-enter",
2254
+ "value": "1"
2255
+ },
2256
+ {
2257
+ "name": "editor-type",
2258
+ "value": "func main() {"
2259
+ },
2260
+ {
2261
+ "name": "editor-enter",
2262
+ "value": "1"
2263
+ },
2264
+ {
2265
+ "name": "author-speak-before",
2266
+ "value": "and let's just print 'Hello world!' to the console."
2267
+ },
2268
+ {
2269
+ "name": "editor-type",
2270
+ "value": ' fmt.Println("Hello, world!")'
2271
+ },
2272
+ {
2273
+ "name": "editor-enter",
2274
+ "value": "1"
2275
+ },
2276
+ {
2277
+ "name": "editor-type",
2278
+ "value": "}"
2279
+ },
2280
+ {
2281
+ "name": "editor-arrow-up",
2282
+ "value": "1"
2283
+ },
2284
+ {
2285
+ "name": "editor-command-right",
2286
+ "value": "1"
2287
+ },
2288
+ {
2289
+ "name": "author-speak-before",
2290
+ "value": "and if I wanted to write the value of some variable to the console, I could do that like so:"
2291
+ },
2292
+ {
2293
+ "name": "editor-backspace",
2294
+ "value": "32"
2295
+ },
2296
+ {
2297
+ "name": "editor-type",
2298
+ "value": " myVariable := 5"
2299
+ },
2300
+ {
2301
+ "name": "editor-enter",
2302
+ "value": "1"
2303
+ },
2304
+ {
2305
+ "name": "editor-type",
2306
+ "value": " fmt.Println(myVariable)"
2307
+ },
2308
+ {
2309
+ "name": "author-speak-before",
2310
+ "value": "Now, when I run this code, I would expect the value of 'myVariable' to be printed to the console. Something like:"
2311
+ },
2312
+ {
2313
+ "name": "editor-enter",
2314
+ "value": "1"
2315
+ },
2316
+ {
2317
+ "name": "editor-type",
2318
+ "value": " // 5"
2319
+ },
2320
+ {
2321
+ "name": "author-speak-before",
2322
+ "value": "Console logging is simple, yet powerful and very useful!"
2323
+ }
2324
+ ]
2325
+ },
2326
+ {
2327
+ keywords: ["python", "python console log", "python log"],
2328
+ actions: [
2329
+ {
2330
+ "name": "author-speak-before",
2331
+ "value": "Let's learn how to use the print function in Python!"
2332
+ },
2333
+ {
2334
+ "name": "author-speak-before",
2335
+ "value": "First, let's make a main.py Python file."
2336
+ },
2337
+ {
2338
+ "name": "mouse-move-file-explorer",
2339
+ "value": "1"
2340
+ },
2341
+ {
2342
+ "name": "mouse-right-click",
2343
+ "value": "1"
2344
+ },
2345
+ {
2346
+ "name": "mouse-move-file-explorer-context-menu-new-file",
2347
+ "value": "1"
2348
+ },
2349
+ {
2350
+ "name": "mouse-left-click",
2351
+ "value": "1"
2352
+ },
2353
+ {
2354
+ "name": "file-explorer-type-new-file-input",
2355
+ "value": "main.py"
2356
+ },
2357
+ {
2358
+ "name": "file-explorer-enter-new-file-input",
2359
+ "value": "1"
2360
+ },
2361
+ {
2362
+ "name": "author-speak-before",
2363
+ "value": "and let's print 'Hello world!' to the console."
2364
+ },
2365
+ {
2366
+ "name": "editor-type",
2367
+ "value": "print('Hello, world!')"
2368
+ },
2369
+ {
2370
+ "name": "author-speak-before",
2371
+ "value": "and if I wanted to write the value of some variable to the console, I could do that like so:"
2372
+ },
2373
+ {
2374
+ "name": "editor-backspace",
2375
+ "value": "22"
2376
+ },
2377
+ {
2378
+ "name": "editor-type",
2379
+ "value": "my_variable = 5"
2380
+ },
2381
+ {
2382
+ "name": "editor-enter",
2383
+ "value": "1"
2384
+ },
2385
+ {
2386
+ "name": "editor-type",
2387
+ "value": "print(my_variable)"
2388
+ },
2389
+ {
2390
+ "name": "author-speak-before",
2391
+ "value": "Now, when I run this code, I would expect the value of 'my_variable' to be printed to the console. Something like:"
2392
+ },
2393
+ {
2394
+ "name": "editor-enter",
2395
+ "value": "1"
2396
+ },
2397
+ {
2398
+ "name": "editor-type",
2399
+ "value": "# 5"
2400
+ },
2401
+ {
2402
+ "name": "author-speak-before",
2403
+ "value": "Console logging is simple, yet powerful and very useful!"
2404
+ },
2405
+ {
2406
+ "name": "author-speak-before",
2407
+ "value": "Well done, lets save our file."
2408
+ },
2409
+ {
2410
+ "name": "editor-save",
2411
+ "value": "main.py"
2412
+ },
2413
+ {
2414
+ "name": "author-speak-before",
2415
+ "value": "Saved!"
2416
+ },
2417
+ {
2418
+ "name": "author-speak-before",
2419
+ "value": "Oh... and I've just realized... this file looks a bit strange... let's add the python shebang to the top of the file."
2420
+ },
2421
+ {
2422
+ "name": "editor-arrow-up",
2423
+ "value": "4"
2424
+ },
2425
+ {
2426
+ "name": "editor-command-left",
2427
+ "value": "1"
2428
+ },
2429
+ {
2430
+ "name": "editor-enter",
2431
+ "value": "1"
2432
+ },
2433
+ {
2434
+ "name": "editor-arrow-up",
2435
+ "value": "1"
2436
+ },
2437
+ {
2438
+ "name": "editor-type",
2439
+ "value": "#!/usr/bin/env python"
2440
+ },
2441
+ {
2442
+ "name": "editor-enter",
2443
+ "value": "1"
2444
+ },
2445
+ {
2446
+ "name": "author-speak-before",
2447
+ "value": "Ah...much better!"
2448
+ },
2449
+ {
2450
+ "name": "author-speak-before",
2451
+ "value": "Let's save the file again."
2452
+ },
2453
+ {
2454
+ "name": "editor-save",
2455
+ "value": "main.py"
2456
+ },
2457
+ {
2458
+ "name": "author-speak-before",
2459
+ "value": "Saved!"
2460
+ },
2461
+ {
2462
+ "name": "author-speak-before",
2463
+ "value": "Now, let's open a terminal and run this file..."
2464
+ },
2465
+ {
2466
+ "name": "terminal-open",
2467
+ "value": "1"
2468
+ },
2469
+ {
2470
+ "name": "terminal-type",
2471
+ "value": "python main.py"
2472
+ },
2473
+ {
2474
+ "name": "terminal-set-output",
2475
+ "value": "5"
2476
+ },
2477
+ {
2478
+ "name": "terminal-enter",
2479
+ "value": "1"
2480
+ },
2481
+ {
2482
+ "name": "author-speak-before",
2483
+ "value": "Perfect, our code is working as expected! I hope you enjoyed this lesson!"
2484
+ }
2485
+ ]
2486
+ }
2487
+ ];
2488
+ var getExampleActionsArrayByKeyword = (keyword) => {
2489
+ const exampleJSONs = [];
2490
+ if (Array.isArray(keyword)) {
2491
+ for (var i = 0; i < keyword.length; i++) {
2492
+ const exampleJSON = getExampleActionsArrayByKeyword(keyword[i]);
2493
+ exampleJSONs.push(exampleJSON);
2494
+ }
2495
+ return exampleJSONs.join(", ");
2496
+ } else {
2497
+ const exampleJSON = getSingleExampleActionsArray(keyword);
2498
+ if (exampleJSON) {
2499
+ return exampleJSON;
2500
+ }
2501
+ }
2502
+ return "";
2503
+ };
2504
+ var getSingleExampleActionsArray = (keyword) => {
2505
+ for (var i = 0; i < keywordsToExampleActionsMap.length; i++) {
2506
+ const keywordMap = keywordsToExampleActionsMap[i];
2507
+ if (keywordMap.keywords.includes(keyword)) {
2508
+ return JSON.stringify(keywordMap.actions);
2509
+ }
2510
+ }
2511
+ return "";
2512
+ };
2513
+
2514
+ // src/resources/get_example_lessons_by_keyword.ts
2515
+ var keywordsToExampleLessonsMap = [
2516
+ {
2517
+ keywords: ["go", "golang", "explore codebase", "existing codebase", "go cli tool"],
2518
+ lesson: {
2519
+ "id": "",
2520
+ "name": "",
2521
+ "description": "",
2522
+ "actions": [
2523
+ {
2524
+ "name": "author-speak-before",
2525
+ "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."
2526
+ },
2527
+ {
2528
+ "name": "mouse-move-file-explorer",
2529
+ "value": "1"
2530
+ },
2531
+ {
2532
+ "name": "mouse-move-file-explorer-file",
2533
+ "value": "main.go"
2534
+ },
2535
+ {
2536
+ "name": "mouse-left-click",
2537
+ "value": "1"
2538
+ },
2539
+ {
2540
+ "name": "author-speak-before",
2541
+ "value": "The main.go file is the entry point for the application. It defines the core functionality for tracking GitLab contributions."
2542
+ },
2543
+ {
2544
+ "name": "mouse-move-to-coordinates-percent",
2545
+ "value": "50,20"
2546
+ },
2547
+ {
2548
+ "name": "author-speak-before",
2549
+ "value": "This struct stores contribution statistics for a user - tracking both commits and merge requests across different time periods."
2550
+ },
2551
+ {
2552
+ "name": "author-speak-before",
2553
+ "value": "The main function initializes the application by loading environment variables, getting the GitLab token, and reading usernames from a file."
2554
+ },
2555
+ {
2556
+ "name": "mouse-move-file-explorer-file",
2557
+ "value": "usernames.txt"
2558
+ },
2559
+ {
2560
+ "name": "mouse-left-click",
2561
+ "value": "1"
2562
+ },
2563
+ {
2564
+ "name": "author-speak-before",
2565
+ "value": "The usernames.txt file contains the list of GitLab users whose contributions we want to track."
2566
+ },
2567
+ {
2568
+ "name": "mouse-move-file-explorer-file",
2569
+ "value": "main.go"
2570
+ },
2571
+ {
2572
+ "name": "mouse-left-click",
2573
+ "value": "1"
2574
+ },
2575
+ {
2576
+ "name": "file-explorer-open-file",
2577
+ "value": "main.go"
2578
+ },
2579
+ {
2580
+ "name": "author-speak-before",
2581
+ "value": "This function calculates contribution statistics for a single user. It defines time windows for different periods - daily, weekly, monthly, and yearly."
2582
+ },
2583
+ {
2584
+ "name": "author-speak-before",
2585
+ "value": "The GetUserActivity function connects to the GitLab API to fetch user events within a specified time period."
2586
+ },
2587
+ {
2588
+ "name": "author-speak-before",
2589
+ "value": "The printResults function formats and displays the contribution statistics, including calculating metrics like average commits per merge request."
2590
+ },
2591
+ {
2592
+ "name": "mouse-move-file-explorer",
2593
+ "value": "1"
2594
+ },
2595
+ {
2596
+ "name": "mouse-move-file-explorer-folder",
2597
+ "value": "http_helper"
2598
+ },
2599
+ {
2600
+ "name": "mouse-left-click",
2601
+ "value": "1"
2602
+ },
2603
+ {
2604
+ "name": "file-explorer-expand-folder",
2605
+ "value": "http_helper"
2606
+ },
2607
+ {
2608
+ "name": "mouse-move-file-explorer-file",
2609
+ "value": "http_helper/http_helper.go"
2610
+ },
2611
+ {
2612
+ "name": "mouse-left-click",
2613
+ "value": "1"
2614
+ },
2615
+ {
2616
+ "name": "file-explorer-open-file",
2617
+ "value": "http_helper/http_helper.go"
2618
+ },
2619
+ {
2620
+ "name": "author-speak-before",
2621
+ "value": "The project has a dedicated HTTP helper module that handles API requests to GitLab."
2622
+ },
2623
+ {
2624
+ "name": "editor-highlight-code",
2625
+ "value": "func MakeHTTPRequest(fullUrl string, httpMethod string, headers map[string]string, queryParameters url.Values, body io.Reader) ([]byte, error) {"
2626
+ },
2627
+ {
2628
+ "name": "author-speak-before",
2629
+ "value": "This function handles standard HTTP requests, while the function below uses generics for typed responses."
2630
+ },
2631
+ {
2632
+ "name": "editor-highlight-code",
2633
+ "value": "func MakeHTTPRequestGeneric[T any](fullUrl string, httpMethod string, headers map[string]string, queryParameters url.Values, body io.Reader, responseType T) (T, error) {"
2634
+ },
2635
+ {
2636
+ "name": "author-speak-before",
2637
+ "value": "MakeHTTPRequestGeneric is a modern Go function using generics to handle typed API responses, making it easier to work with the GitLab API."
2638
+ },
2639
+ {
2640
+ "name": "mouse-move-file-explorer-file",
2641
+ "value": "main_test.go"
2642
+ },
2643
+ {
2644
+ "name": "mouse-left-click",
2645
+ "value": "1"
2646
+ },
2647
+ {
2648
+ "name": "file-explorer-open-file",
2649
+ "value": "main_test.go"
2650
+ },
2651
+ {
2652
+ "name": "author-speak-before",
2653
+ "value": "The project also includes tests to verify the functionality of the GitLab API interactions."
2654
+ },
2655
+ {
2656
+ "name": "mouse-move-file-explorer-file",
2657
+ "value": ".env"
2658
+ },
2659
+ {
2660
+ "name": "mouse-left-click",
2661
+ "value": "1"
2662
+ },
2663
+ {
2664
+ "name": "file-explorer-open-file",
2665
+ "value": ".env"
2666
+ },
2667
+ {
2668
+ "name": "author-speak-before",
2669
+ "value": "The .env file stores the GitLab token needed to authenticate with the API."
2670
+ },
2671
+ {
2672
+ "name": "mouse-move-file-explorer-file",
2673
+ "value": "go.mod"
2674
+ },
2675
+ {
2676
+ "name": "mouse-left-click",
2677
+ "value": "1"
2678
+ },
2679
+ {
2680
+ "name": "file-explorer-open-file",
2681
+ "value": "go.mod"
2682
+ },
2683
+ {
2684
+ "name": "author-speak-before",
2685
+ "value": "The go.mod file lists the project dependencies, including the go-gitlab library for API interaction and godotenv for environment variable loading."
2686
+ },
2687
+ {
2688
+ "name": "mouse-move-file-explorer-file",
2689
+ "value": "main.go"
2690
+ },
2691
+ {
2692
+ "name": "mouse-left-click",
2693
+ "value": "1"
2694
+ },
2695
+ {
2696
+ "name": "file-explorer-open-file",
2697
+ "value": "main.go"
2698
+ },
2699
+ {
2700
+ "name": "author-speak-after",
2701
+ "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."
2702
+ }
2703
+ ],
2704
+ "initialSnapshot": {
2705
+ "isUnsavedChangesDialogOpen": false,
2706
+ "unsavedFileName": "",
2707
+ "fileExplorerSnapshot": {
2708
+ "isFileExplorerContextMenuOpen": false,
2709
+ "isFileContextMenuOpen": false,
2710
+ "isFolderContextMenuOpen": false,
2711
+ "isNewFileInputVisible": false,
2712
+ "isNewFolderInputVisible": false,
2713
+ "isRenameFileInputVisible": false,
2714
+ "isRenameFolderInputVisible": false,
2715
+ "newFileInputValue": "",
2716
+ "newFolderInputValue": "",
2717
+ "renameFileInputValue": "",
2718
+ "renameFolderInputValue": "",
2719
+ "originalFileBeingRenamed": "",
2720
+ "originalFolderBeingRenamed": "",
2721
+ "newFileParentPath": "",
2722
+ "newFolderParentPath": "",
2723
+ "fileStructure": {
2724
+ ".env": {
2725
+ "type": "file",
2726
+ "content": "GITLAB_TOKEN=glpat-uShSYUHwpUcDxfkGcoNL",
2727
+ "language": "plaintext",
2728
+ "caretPosition": {
2729
+ "row": 0,
2730
+ "col": 0
2731
+ }
2732
+ },
2733
+ ".gitignore": {
2734
+ "type": "file",
2735
+ "content": ".env",
2736
+ "language": "plaintext",
2737
+ "caretPosition": {
2738
+ "row": 0,
2739
+ "col": 0
2740
+ }
2741
+ },
2742
+ "go.mod": {
2743
+ "type": "file",
2744
+ "content": "module solve.ch/solvers\n\ngo 1.23.0\n\nrequire github.com/xanzy/go-gitlab v0.115.0\n\nrequire (\n github.com/golang/protobuf v1.5.3 // indirect\n github.com/google/go-querystring v1.1.0 // indirect\n github.com/hashicorp/go-cleanhttp v0.5.2 // indirect\n github.com/hashicorp/go-retryablehttp v0.7.7 // indirect\n github.com/joho/godotenv v1.5.1\n gitlab.com/gitlab-org/api/client-go v0.120.0\n golang.org/x/net v0.8.0 // indirect\n golang.org/x/oauth2 v0.24.0 // indirect\n golang.org/x/time v0.8.0 // indirect\n google.golang.org/appengine v1.6.7 // indirect\n google.golang.org/protobuf v1.29.1 // indirect\n)\n",
2745
+ "language": "plaintext",
2746
+ "caretPosition": {
2747
+ "row": 0,
2748
+ "col": 0
2749
+ }
2750
+ },
2751
+ "go.sum": {
2752
+ "type": "file",
2753
+ "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",
2754
+ "language": "plaintext",
2755
+ "caretPosition": {
2756
+ "row": 0,
2757
+ "col": 0
2758
+ }
2759
+ },
2760
+ "http_helper": {
2761
+ "type": "directory",
2762
+ "content": "",
2763
+ "collapsed": false,
2764
+ "children": {
2765
+ "http_helper.go": {
2766
+ "type": "file",
2767
+ "content": `package http_helper
2768
+
2769
+ import (
2770
+ "encoding/json"
2771
+ "fmt"
2772
+ "io"
2773
+ "log"
2774
+ "net/http"
2775
+ "net/url"
2776
+ "strings"
2777
+ )
2778
+
2779
+ // in the case of GET, the parameter queryParameters is transferred to the URL as query parameters
2780
+ // in the case of POST, the parameter body, an io.Reader, is used
2781
+ func MakeHTTPRequest(fullUrl string, httpMethod string, headers map[string]string, queryParameters url.Values, body io.Reader) ([]byte, error) {
2782
+ client := http.Client{}
2783
+ u, err := url.Parse(fullUrl)
2784
+ if err != nil {
2785
+ return nil, err
2786
+ }
2787
+
2788
+ // if it's a GET, we need to append the query parameters.
2789
+ if httpMethod == "GET" {
2790
+ q := u.Query()
2791
+
2792
+ for k, v := range queryParameters {
2793
+ // this depends on the type of api, you may need to do it for each of v
2794
+ q.Set(k, strings.Join(v, ","))
2795
+ }
2796
+ // set the query to the encoded parameters
2797
+ u.RawQuery = q.Encode()
2798
+ }
2799
+
2800
+ // regardless of GET or POST, we can safely add the body
2801
+ req, err := http.NewRequest(httpMethod, u.String(), body)
2802
+ if err != nil {
2803
+ return nil, err
2804
+ }
2805
+
2806
+ // for each header passed, add the header value to the request
2807
+ for k, v := range headers {
2808
+ req.Header.Set(k, v)
2809
+ }
2810
+
2811
+ // optional: log the request for easier stack tracing
2812
+ log.Printf("%s %s\\n", httpMethod, req.URL.String())
2813
+
2814
+ // finally, do the request
2815
+ res, err := client.Do(req)
2816
+ if err != nil {
2817
+ return nil, err
2818
+ }
2819
+
2820
+ if res == nil {
2821
+ return nil, fmt.Errorf("error: calling %s returned empty response", u.String())
2822
+ }
2823
+
2824
+ responseData, err := io.ReadAll(res.Body)
2825
+ if err != nil {
2826
+ return nil, err
2827
+ }
2828
+
2829
+ defer res.Body.Close()
2830
+
2831
+ if res.StatusCode != http.StatusOK {
2832
+ return nil, fmt.Errorf("error calling %s:\\nstatus: %s\\nresponseData: %s", u.String(), res.Status, responseData)
2833
+ }
2834
+
2835
+ return responseData, nil
2836
+ }
2837
+
2838
+ func MakeHTTPRequestGeneric[T any](fullUrl string, httpMethod string, headers map[string]string, queryParameters url.Values, body io.Reader, responseType T) (T, error) {
2839
+ client := http.Client{}
2840
+ u, err := url.Parse(fullUrl)
2841
+ if err != nil {
2842
+ return responseType, err
2843
+ }
2844
+
2845
+ // if it's a GET, we need to append the query parameters.
2846
+ if httpMethod == "GET" {
2847
+ q := u.Query()
2848
+
2849
+ for k, v := range queryParameters {
2850
+ // this depends on the type of api, you may need to do it for each of v
2851
+ q.Set(k, strings.Join(v, ","))
2852
+ }
2853
+ // set the query to the encoded parameters
2854
+ u.RawQuery = q.Encode()
2855
+ }
2856
+
2857
+ // regardless of GET or POST, we can safely add the body
2858
+ req, err := http.NewRequest(httpMethod, u.String(), body)
2859
+ if err != nil {
2860
+ return responseType, err
2861
+ }
2862
+
2863
+ // for each header passed, add the header value to the request
2864
+ for k, v := range headers {
2865
+ req.Header.Set(k, v)
2866
+ }
2867
+
2868
+ // optional: log the request for easier stack tracing
2869
+ log.Printf("%s %s\\n", httpMethod, req.URL.String())
2870
+
2871
+ // finally, do the request
2872
+ res, err := client.Do(req)
2873
+ if err != nil {
2874
+ return responseType, err
2875
+ }
2876
+
2877
+ if res == nil {
2878
+ return responseType, fmt.Errorf("error: calling %s returned empty response", u.String())
2879
+ }
2880
+
2881
+ responseData, err := io.ReadAll(res.Body)
2882
+ if err != nil {
2883
+ return responseType, err
2884
+ }
2885
+
2886
+ defer res.Body.Close()
2887
+
2888
+ if res.StatusCode != http.StatusOK {
2889
+ return responseType, fmt.Errorf("error calling %s:\\nstatus: %s\\nresponseData: %s", u.String(), res.Status, responseData)
2890
+ }
2891
+
2892
+ var responseObject T
2893
+ err = json.Unmarshal(responseData, &responseObject)
2894
+
2895
+ if err != nil {
2896
+ log.Printf("error unmarshaling response: %+v", err)
2897
+ return responseType, err
2898
+ }
2899
+
2900
+ return responseObject, nil
2901
+ }
2902
+ `,
2903
+ "language": "Go",
2904
+ "caretPosition": {
2905
+ "row": 0,
2906
+ "col": 0
2907
+ }
2908
+ }
2909
+ }
2910
+ },
2911
+ "main.go": {
2912
+ "type": "file",
2913
+ "content": 'package main\n\nimport (\n "bufio"\n "fmt"\n "log"\n "os"\n "sort"\n "strings"\n "time"\n\n "github.com/joho/godotenv"\n "solve.ch/solvers/http_helper"\n)\n\ntype ContributionStats struct {\n Username string\n DailyCommits int\n WeeklyCommits int\n MonthlyCommits int\n YearlyCommits int\n DailyMRs int\n WeeklyMRs int\n MonthlyMRs int\n YearlyMRs int\n // New field to store total pages fetched from GitLab\n PagesFetched int\n}\n\ntype TimePeriod struct {\n Start time.Time\n End time.Time\n}\n\ntype GitLabAuthor struct {\n ID int `json:"id"`\n Username string `json:"username"`\n Name string `json:"name"`\n State string `json:"state"`\n AvatarURL string `json:"avatar_url"`\n WebURL string `json:"web_url"`\n}\n\ntype GitLabPushData struct {\n CommitCount int `json:"commit_count"`\n Action string `json:"action"`\n RefType string `json:"ref_type"`\n CommitFrom string `json:"commit_from"`\n CommitTo string `json:"commit_to"`\n Ref string `json:"ref"`\n CommitTitle string `json:"commit_title"`\n RefCount *int `json:"ref_count"`\n}\n\ntype GitLabEvent struct {\n ID int `json:"id"`\n ProjectID int `json:"project_id"`\n ActionName string `json:"action_name"`\n TargetID *int `json:"target_id"`\n TargetIID *int `json:"target_iid"`\n TargetType *string `json:"target_type"`\n AuthorID int `json:"author_id"`\n TargetTitle *string `json:"target_title"`\n CreatedAt time.Time `json:"created_at"`\n Author GitLabAuthor `json:"author"`\n PushData *GitLabPushData `json:"push_data"`\n}\n\nfunc main() {\n // Load .env file\n err := godotenv.Load()\n if err != nil {\n log.Println("Warning: Error loading .env file:", err)\n }\n\n // Replace with your GitLab token\n token := os.Getenv("GITLAB_TOKEN")\n if token == "" {\n log.Fatal("GITLAB_TOKEN environment variable not set")\n }\n\n // Read usernames from file\n usernames, err := readUsernames("usernames.txt")\n if err != nil {\n log.Fatalf("Failed to read usernames: %v", err)\n }\n\n // Get contribution stats for each user\n stats := make([]ContributionStats, 0)\n for _, username := range usernames {\n stat, err := getContributionStats(token, username)\n if err != nil {\n log.Printf("Warning: Failed to get stats for %s: %v", username, err)\n continue\n }\n stats = append(stats, stat)\n }\n\n // Sort by yearly commits (could be modified to use different metrics)\n sort.Slice(stats, func(i, j int) bool {\n return stats[i].YearlyCommits > stats[j].YearlyCommits\n })\n\n // Print results with normalized bars\n printResults(stats)\n}\n\nfunc readUsernames(filename string) ([]string, error) {\n file, err := os.Open(filename)\n if err != nil {\n return nil, err\n }\n defer file.Close()\n\n var usernames []string\n scanner := bufio.NewScanner(file)\n for scanner.Scan() {\n username := strings.TrimSpace(scanner.Text())\n if username != "" {\n usernames = append(usernames, username)\n }\n }\n return usernames, scanner.Err()\n}\n\nfunc getContributionStats(token, username string) (ContributionStats, error) {\n stats := ContributionStats{Username: username}\n\n // Define time windows for daily, weekly, monthly, yearly\n todayStart := time.Now().Truncate(24 * time.Hour)\n todayEnd := todayStart.Add(24 * time.Hour)\n\n // last 7 days (including today)\n weekStart := todayStart.AddDate(0, 0, -6)\n weekEnd := todayEnd\n\n // last 1 month\n monthStart := todayStart.AddDate(0, -1, 0)\n monthEnd := todayEnd\n\n // last 1 year\n yearStart := todayStart.AddDate(-1, 0, 0)\n yearEnd := todayEnd\n\n periods := map[string]TimePeriod{\n "daily": {\n Start: todayStart,\n End: todayEnd,\n },\n "weekly": {\n Start: weekStart,\n End: weekEnd,\n },\n "monthly": {\n Start: monthStart,\n End: monthEnd,\n },\n "yearly": {\n Start: yearStart,\n End: yearEnd,\n },\n }\n\n // We\'ll track how many pages we fetched total for each user\n totalPages := 0\n\n // Get user\'s events for each time period\n for period, timeRange := range periods {\n commits, mrs, pages, err := GetUserActivity(token, username, timeRange.Start, timeRange.End)\n if err != nil {\n return stats, err\n }\n // Accumulate total page count\n totalPages += pages\n\n switch period {\n case "daily":\n stats.DailyCommits = commits\n stats.DailyMRs = mrs\n case "weekly":\n stats.WeeklyCommits = commits\n stats.WeeklyMRs = mrs\n case "monthly":\n stats.MonthlyCommits = commits\n stats.MonthlyMRs = mrs\n case "yearly":\n stats.YearlyCommits = commits\n stats.YearlyMRs = mrs\n }\n }\n\n // Store the total pages fetched in stats\n stats.PagesFetched = totalPages\n\n return 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 baseURL := "https://git.solve.ch/api/v4/users"\n // We will iterate over pages, so exclude &page from the base URL:\n fullURL := fmt.Sprintf(\n "%s/%s/events?after=%s&before=%s&per_page=100",\n baseURL,\n username,\n start.Format("2006-01-02"),\n end.Format("2006-01-02"),\n )\n\n headers := map[string]string{\n "PRIVATE-TOKEN": token,\n }\n\n var allEvents []GitLabEvent\n page := 1\n\n for {\n // Construct URL for the current page\n pagedURL := fmt.Sprintf("%s&page=%d", fullURL, page)\n\n var tempEvents []GitLabEvent\n tempEvents, err = http_helper.MakeHTTPRequestGeneric(\n pagedURL,\n "GET",\n headers,\n nil,\n nil,\n tempEvents,\n )\n if err != nil {\n return 0, 0, 0, fmt.Errorf("failed to get user events (page %d): %v", page, err)\n }\n\n // If no events, we\'ve reached the end\n if len(tempEvents) == 0 {\n break\n }\n\n // Accumulate events\n allEvents = append(allEvents, tempEvents...)\n page++\n }\n\n // The page - 1 is how many pages we actually fetched\n totalPages = page - 1\n\n // Count commits and MRs\n for _, event := range allEvents {\n switch {\n case event.ActionName == "pushed to" || event.ActionName == "pushed new":\n if event.PushData != nil {\n commits += event.PushData.CommitCount\n }\n case event.ActionName == "accepted" && event.TargetType != nil && *event.TargetType == "MergeRequest":\n mrs++\n case event.ActionName == "opened" && event.TargetType != nil && *event.TargetType == "MergeRequest":\n mrs++\n default:\n // TODO: define a \'miscellaneous\' category for other types of events\n commits++\n }\n }\n\n return commits, mrs, totalPages, nil\n}\n\nfunc printResults(stats []ContributionStats) {\n\n // loop over stats to get the average number of commits per merge request\n var totalMRs, totalCommits int\n for _, stat := range stats {\n totalMRs += stat.YearlyMRs\n totalCommits += stat.YearlyCommits\n }\n\n averageCommitsPerMR := int(float64(totalCommits) / float64(totalMRs))\n\n // Find maximum values for normalization\n var maxDaily, maxWeekly, maxMonthly, maxYearly int\n for _, stat := range stats {\n maxDaily = max(maxDaily, stat.DailyCommits+(stat.DailyMRs*averageCommitsPerMR))\n maxWeekly = max(maxWeekly, stat.WeeklyCommits+(stat.WeeklyMRs*averageCommitsPerMR))\n maxMonthly = max(maxMonthly, stat.MonthlyCommits+(stat.MonthlyMRs*averageCommitsPerMR))\n maxYearly = max(maxYearly, stat.YearlyCommits+(stat.YearlyMRs*averageCommitsPerMR))\n }\n\n var maxDailyCommits, maxWeeklyCommits, maxMonthlyCommits, maxYearlyCommits int\n for _, stat := range stats {\n maxDailyCommits = max(maxDailyCommits, stat.DailyCommits)\n maxWeeklyCommits = max(maxWeeklyCommits, stat.WeeklyCommits)\n maxMonthlyCommits = max(maxMonthlyCommits, stat.MonthlyCommits)\n maxYearlyCommits = max(maxYearlyCommits, stat.YearlyCommits)\n }\n\n var maxDailyMRs, maxWeeklyMRs, maxMonthlyMRs, maxYearlyMRs int\n for _, stat := range stats {\n maxDailyMRs = max(maxDailyMRs, stat.DailyMRs)\n maxWeeklyMRs = max(maxWeeklyMRs, stat.WeeklyMRs)\n maxMonthlyMRs = max(maxMonthlyMRs, stat.MonthlyMRs)\n maxYearlyMRs = max(maxYearlyMRs, stat.YearlyMRs)\n }\n\n // sort stats by scaled contributions (commts + scaled MRs)\n sort.Slice(stats, func(i, j int) bool {\n return (stats[i].YearlyCommits + stats[i].YearlyMRs*averageCommitsPerMR) > (stats[j].YearlyCommits + stats[j].YearlyMRs*averageCommitsPerMR)\n })\n\n fmt.Println("\\n\u{1F3C6} Top Contributors Overall\\n")\n fmt.Printf("(MRs scaled by %dx, the global average commits per MR across Solve)\\n", averageCommitsPerMR)\n fmt.Println("========================")\n\n emojisByRank := []string{"\u{1F947}", "\u{1F948}", "\u{1F949}"}\n\n for index, stat := range stats {\n // Use the index to get the corresponding emoji - if we are less than three, use the list\n emoji := "\u{1F468}\u200D\u{1F4BB}"\n if index < len(emojisByRank) {\n emoji = emojisByRank[index]\n }\n fmt.Printf("\\n%s %s\\n", emoji, stat.Username)\n fmt.Printf(" (Fetched %d pages from GitLab)\\n", stat.PagesFetched)\n\n // Print daily, weekly, monthly, yearly bars\n printBar("Today ", stat.DailyCommits+(stat.DailyMRs*averageCommitsPerMR), maxDaily)\n printBar("Week ", stat.WeeklyCommits+(stat.WeeklyMRs*averageCommitsPerMR), maxWeekly)\n printBar("Month ", stat.MonthlyCommits+(stat.MonthlyMRs*averageCommitsPerMR), maxMonthly)\n printBar("Year ", stat.YearlyCommits+(stat.YearlyMRs*averageCommitsPerMR), maxYearly)\n\n fmt.Printf("Details: %d commits, %d MRs in the past year. Total Score: %d\\n",\n stat.YearlyCommits, stat.YearlyMRs, stat.YearlyCommits+(stat.YearlyMRs*averageCommitsPerMR))\n }\n\n // sort stats by commits now\n sort.Slice(stats, func(i, j int) bool {\n return stats[i].YearlyCommits > stats[j].YearlyCommits\n })\n\n fmt.Println("\\n\u{1F3C6} Top Committers")\n fmt.Println("========================")\n\n for index, stat := range stats {\n // Use the index to get the corresponding emoji - if we are less than three, use the list\n emoji := "\u{1F468}\u200D\u{1F4BB}"\n if index < len(emojisByRank) {\n emoji = emojisByRank[index]\n }\n fmt.Printf("\\n%s %s\\n", emoji, stat.Username)\n fmt.Printf(" (Fetched %d pages from GitLab)\\n", stat.PagesFetched)\n\n // Print daily, weekly, monthly, yearly bars\n printBar("Today ", stat.DailyCommits, maxDailyCommits)\n printBar("Week ", stat.WeeklyCommits, maxWeeklyCommits)\n printBar("Month ", stat.MonthlyCommits, maxMonthlyCommits)\n printBar("Year ", stat.YearlyCommits, maxYearlyCommits)\n\n fmt.Printf("Details: %d commits in the past year\\n",\n stat.YearlyCommits)\n }\n\n // sort stats by MRs now\n sort.Slice(stats, func(i, j int) bool {\n return stats[i].YearlyMRs > stats[j].YearlyMRs\n })\n\n fmt.Println("\\n\u{1F3C6} Top Merge Requesters")\n fmt.Println("========================")\n\n for index, stat := range stats {\n // Use the index to get the corresponding emoji - if we are less than three, use the list\n emoji := "\u{1F468}\u200D\u{1F4BB}"\n if index < len(emojisByRank) {\n emoji = emojisByRank[index]\n }\n fmt.Printf("\\n%s %s\\n", emoji, stat.Username)\n fmt.Printf(" (Fetched %d pages from GitLab)\\n", stat.PagesFetched)\n\n // Print daily, weekly, monthly, yearly bars\n printBar("Today ", stat.DailyMRs, maxDailyMRs)\n printBar("Week ", stat.WeeklyMRs, maxWeeklyMRs)\n printBar("Month ", stat.MonthlyMRs, maxMonthlyMRs)\n printBar("Year ", stat.YearlyMRs, maxYearlyMRs)\n\n fmt.Printf("Details: %d MRs in the past year\\n",\n stat.YearlyMRs)\n }\n}\n\nfunc printBar(label string, value, maxValue int) {\n const barWidth = 30\n var percentage float64\n if maxValue > 0 {\n percentage = float64(value) / float64(maxValue) * 100\n }\n\n filledWidth := int(float64(barWidth) * percentage / 100)\n bar := strings.Repeat("\u2588", filledWidth) + strings.Repeat("\u2591", barWidth-filledWidth)\n\n fmt.Printf("%s [%s] %.1f%% (%d)\\n", label, bar, percentage, value)\n}\n\nfunc max(a, b int) int {\n if a > b {\n return a\n }\n return b\n}\n',
2914
+ "language": "Go",
2915
+ "caretPosition": {
2916
+ "row": 0,
2917
+ "col": 0
2918
+ }
2919
+ },
2920
+ "main_test.go": {
2921
+ "type": "file",
2922
+ "content": `package main
2923
+
2924
+ import (
2925
+ "log"
2926
+ "os"
2927
+ "testing"
2928
+ "time"
2929
+
2930
+ "github.com/joho/godotenv"
2931
+ "solve.ch/solvers/http_helper"
2932
+ )
2933
+
2934
+ func TestGetUserActivity(t *testing.T) {
2935
+ // Load .env file
2936
+ err := godotenv.Load()
2937
+ if err != nil {
2938
+ log.Println("Warning: Error loading .env file:", err)
2939
+ }
2940
+
2941
+ // Get GitLab token from environment
2942
+ token := os.Getenv("GITLAB_TOKEN")
2943
+ if token == "" {
2944
+ t.Fatal("GITLAB_TOKEN environment variable not set")
2945
+ }
2946
+
2947
+ // Set up time range for last 7 days
2948
+ now := time.Now()
2949
+ sevenDaysAgo := now.AddDate(0, 0, -7)
2950
+
2951
+ // Test GetUserActivity function
2952
+ commits, mrs, err := GetUserActivity(token, "cfrewin", sevenDaysAgo, now)
2953
+ if err != nil {
2954
+ t.Fatalf("GetUserActivity failed: %v", err)
2955
+ }
2956
+
2957
+ // Log the results
2958
+ t.Logf("Activity for user 'cfrewin' in the last 7 days:")
2959
+ t.Logf("Commits: %d", commits)
2960
+ t.Logf("Merge Requests: %d", mrs)
2961
+
2962
+ // From the sample JSON, we can see several commits and MRs
2963
+ // Let's add some basic assertions
2964
+ if commits == 0 {
2965
+ t.Log("Warning: No commits found in the last 7 days")
2966
+ }
2967
+ if mrs < 0 {
2968
+ t.Error("Number of merge requests should not be negative")
2969
+ }
2970
+
2971
+ // Add detailed logging of commits by looking at the raw response
2972
+ responseData, err := http_helper.MakeHTTPRequest(
2973
+ "https://git.solve.ch/api/v4/users/cfrewin/events",
2974
+ "GET",
2975
+ map[string]string{"PRIVATE-TOKEN": token},
2976
+ nil,
2977
+ nil,
2978
+ )
2979
+ if err != nil {
2980
+ t.Logf("Failed to get raw events: %v", err)
2981
+ } else {
2982
+ t.Logf("Raw response: %s", string(responseData))
2983
+ }
2984
+ }
2985
+ `,
2986
+ "language": "Go",
2987
+ "caretPosition": {
2988
+ "row": 0,
2989
+ "col": 0
2990
+ }
2991
+ },
2992
+ "usernames.txt": {
2993
+ "type": "file",
2994
+ "content": "cfrewin\nsgacond\nthuber\nsgulas\nvkamberi\nurust\nsreber\nsrupp\nmsobotka\ndavid.ritter\nrlauber\nmkohler\nakati\nmhemmi\njheron",
2995
+ "language": "Plain Text",
2996
+ "caretPosition": {
2997
+ "row": 0,
2998
+ "col": 0
2999
+ }
833
3000
  }
834
- ]
835
- },
836
- {
837
- keywords: ["python", "python console log", "python log"],
838
- actions: [
839
- {
840
- "name": "author-speak-before",
841
- "value": "Let's learn how to use the print function in Python!"
842
- },
843
- {
844
- "name": "author-speak-before",
845
- "value": "First, let's make a main.py Python file."
846
- },
847
- {
848
- "name": "mouse-move-file-explorer",
849
- "value": "1"
850
- },
851
- {
852
- "name": "mouse-right-click",
853
- "value": "1"
854
- },
855
- {
856
- "name": "mouse-move-file-explorer-context-menu-new-file",
857
- "value": "1"
858
- },
859
- {
860
- "name": "mouse-left-click",
861
- "value": "1"
862
- },
863
- {
864
- "name": "file-explorer-type-new-file-input",
865
- "value": "main.py"
866
- },
867
- {
868
- "name": "file-explorer-enter-new-file-input",
869
- "value": "1"
870
- },
871
- {
872
- "name": "author-speak-before",
873
- "value": "and let's print 'Hello world!' to the console."
874
- },
875
- {
876
- "name": "editor-type",
877
- "value": "print('Hello, world!')"
878
- },
879
- {
880
- "name": "author-speak-before",
881
- "value": "and if I wanted to write the value of some variable to the console, I could do that like so:"
882
- },
883
- {
884
- "name": "editor-backspace",
885
- "value": "22"
886
- },
887
- {
888
- "name": "editor-type",
889
- "value": "my_variable = 5"
890
- },
891
- {
892
- "name": "editor-enter",
893
- "value": "1"
894
- },
895
- {
896
- "name": "editor-type",
897
- "value": "print(my_variable)"
898
- },
899
- {
900
- "name": "author-speak-before",
901
- "value": "Now, when I run this code, I would expect the value of 'my_variable' to be printed to the console. Something like:"
902
- },
903
- {
904
- "name": "editor-enter",
905
- "value": "1"
906
- },
907
- {
908
- "name": "editor-type",
909
- "value": "# 5"
910
- },
911
- {
912
- "name": "author-speak-before",
913
- "value": "Console logging is simple, yet powerful and very useful!"
914
- },
915
- {
916
- "name": "author-speak-before",
917
- "value": "Well done, lets save our file."
918
- },
919
- {
920
- "name": "editor-save",
921
- "value": "main.py"
922
- },
923
- {
924
- "name": "author-speak-before",
925
- "value": "Saved!"
926
- },
927
- {
928
- "name": "author-speak-before",
929
- "value": "Oh... and I've just realized... this file looks a bit strange... let's add the python shebang to the top of the file."
930
- },
931
- {
932
- "name": "editor-arrow-up",
933
- "value": "4"
934
- },
935
- {
936
- "name": "editor-command-left",
937
- "value": "1"
938
- },
939
- {
940
- "name": "editor-enter",
941
- "value": "1"
942
- },
943
- {
944
- "name": "editor-arrow-up",
945
- "value": "1"
946
- },
947
- {
948
- "name": "editor-type",
949
- "value": "#!/usr/bin/env python"
950
- },
951
- {
952
- "name": "editor-enter",
953
- "value": "1"
954
- },
955
- {
956
- "name": "author-speak-before",
957
- "value": "Ah...much better!"
958
- },
959
- {
960
- "name": "author-speak-before",
961
- "value": "Let's save the file again."
962
- },
963
- {
964
- "name": "editor-save",
965
- "value": "main.py"
966
- },
967
- {
968
- "name": "author-speak-before",
969
- "value": "Saved!"
970
- },
971
- {
972
- "name": "author-speak-before",
973
- "value": "Now, let's open a terminal and run this file..."
974
- },
975
- {
976
- "name": "terminal-open",
977
- "value": "1"
978
- },
979
- {
980
- "name": "terminal-type",
981
- "value": "python main.py"
982
- },
983
- {
984
- "name": "terminal-set-output",
985
- "value": "5"
986
- },
987
- {
988
- "name": "terminal-enter",
989
- "value": "1"
990
- },
3001
+ }
3002
+ },
3003
+ "editorSnapshot": {
3004
+ "isEditorContextMenuOpen": false,
3005
+ "editors": []
3006
+ },
3007
+ "terminalSnapshot": {
3008
+ "terminals": []
3009
+ },
3010
+ "mouseSnapshot": {
3011
+ "location": "editor",
3012
+ "currentHoveredFileName": "",
3013
+ "currentHoveredFolderName": "",
3014
+ "currentHoveredEditorTabFileName": "",
3015
+ "x": 0,
3016
+ "y": 0,
3017
+ "timestamp": 0,
3018
+ "type": "move",
3019
+ "buttonStates": {
3020
+ "left": false,
3021
+ "right": false,
3022
+ "middle": false
3023
+ },
3024
+ "scrollPosition": {
3025
+ "x": 0,
3026
+ "y": 0
3027
+ }
3028
+ },
3029
+ "authorSnapshot": {
3030
+ "authors": [
991
3031
  {
992
- "name": "author-speak-before",
993
- "value": "Perfect, our code is working as expected! I hope you enjoyed this lesson!"
3032
+ "currentSpeechCaption": ""
994
3033
  }
995
- ]
996
- },
997
- ];
998
- // returns a JSON representation of an example lesson based on keywords
999
- // the LLM can use this to learn real example lessons created by humans
1000
- const getExampleLesson = (keyword) => {
1001
- const exampleJSONs = [];
1002
- if (Array.isArray(keyword)) {
1003
- // if the keyword is an array, loop over all keywords and get the example lesson for each one
1004
- for (var i = 0; i < keyword.length; i++) {
1005
- const exampleJSON = getExampleLesson(keyword[i]);
1006
- exampleJSONs.push(exampleJSON);
3034
+ ]
1007
3035
  }
1008
- return exampleJSONs.join(", ");
3036
+ }
1009
3037
  }
1010
- else {
1011
- // if the keyword is a string, get the example lesson for that keyword
1012
- const exampleJSON = getSingleExampleLesson(keyword);
1013
- if (exampleJSON) {
1014
- return exampleJSON;
1015
- }
3038
+ }
3039
+ ];
3040
+ var getExampleLessonByKeyword = (keyword) => {
3041
+ const exampleJSONs = [];
3042
+ if (Array.isArray(keyword)) {
3043
+ for (var i = 0; i < keyword.length; i++) {
3044
+ const exampleJSON = getExampleLessonByKeyword(keyword[i]);
3045
+ exampleJSONs.push(exampleJSON);
1016
3046
  }
1017
- // if no keywords match, return an empty string
1018
- return "";
1019
- };
1020
- const getSingleExampleLesson = (keyword) => {
1021
- // loop over all keywords and find the first one that matches
1022
- for (var i = 0; i < keywordsToExampleMap.length; i++) {
1023
- const keywordMap = keywordsToExampleMap[i];
1024
- if (keywordMap.keywords.includes(keyword)) {
1025
- return JSON.stringify(keywordMap.actions);
1026
- }
3047
+ return exampleJSONs.join(", ");
3048
+ } else {
3049
+ const exampleJSON = getSingleExampleLesson(keyword);
3050
+ if (exampleJSON) {
3051
+ return exampleJSON;
3052
+ }
3053
+ }
3054
+ return "";
3055
+ };
3056
+ var getSingleExampleLesson = (keyword) => {
3057
+ for (var i = 0; i < keywordsToExampleLessonsMap.length; i++) {
3058
+ const keywordMap = keywordsToExampleLessonsMap[i];
3059
+ if (keywordMap.keywords.includes(keyword)) {
3060
+ return JSON.stringify(keywordMap.lesson, null, 2);
1027
3061
  }
1028
- // if no keywords match, return an empty string
1029
- return "";
3062
+ }
3063
+ return "";
1030
3064
  };
1031
3065
 
1032
- // Load environment variables from .env file
3066
+ // src/index.ts
1033
3067
  dotenv.config();
1034
- const ActionSchema = z.object({
1035
- name: z.string(),
1036
- value: z.string()
3068
+ var ActionSchema = z2.object({
3069
+ name: z2.string(),
3070
+ value: z2.string()
1037
3071
  });
1038
- const CourseSnapshotSchema = z.object({});
1039
- const LessonSchema = z.object({
1040
- id: z.string(),
1041
- name: z.string(),
1042
- description: z.string(),
1043
- actions: z.array(ActionSchema),
1044
- initialSnapshot: CourseSnapshotSchema,
1045
- finalSnapshot: CourseSnapshotSchema
3072
+ var validActionNames = new Set(AllActionStrings);
3073
+ var StrictActionSchema = z2.object({
3074
+ name: z2.string().refine((name) => validActionNames.has(name), "unknown CodeVideo action name"),
3075
+ value: z2.string().min(1, "action 'value' must not be empty")
3076
+ }).refine(
3077
+ (a) => !isRepeatableAction2(a) || /^[0-9]+$/.test(a.value),
3078
+ { message: 'repeatable actions require a positive integer string value (usually "1")' }
3079
+ );
3080
+ var CourseSnapshotSchema = z2.custom((value) => {
3081
+ if (!value || typeof value !== "object") return false;
3082
+ const snapshot = value;
3083
+ return typeof snapshot.isUnsavedChangesDialogOpen === "boolean" && typeof snapshot.unsavedFileName === "string" && !!snapshot.fileExplorerSnapshot && !!snapshot.editorSnapshot && !!snapshot.terminalSnapshot && !!snapshot.mouseSnapshot && !!snapshot.authorSnapshot;
3084
+ }, "Invalid CodeVideo course snapshot");
3085
+ var LessonSchema = z2.object({
3086
+ id: z2.string(),
3087
+ name: z2.string(),
3088
+ description: z2.string(),
3089
+ actions: z2.array(ActionSchema),
3090
+ initialSnapshot: CourseSnapshotSchema,
3091
+ finalSnapshot: CourseSnapshotSchema
3092
+ });
3093
+ var CourseSchema = z2.object({
3094
+ id: z2.string(),
3095
+ name: z2.string(),
3096
+ description: z2.string(),
3097
+ primaryLanguage: z2.string(),
3098
+ lessons: z2.array(LessonSchema)
1046
3099
  });
1047
- const CourseSchema = z.object({
1048
- id: z.string(),
1049
- name: z.string(),
1050
- description: z.string(),
1051
- primaryLanguage: z.string(),
1052
- lessons: z.array(LessonSchema)
3100
+ var RenderOptionsParam = RenderOptionsSchema.optional().describe("Render options: orientation, IDE layout (theme, font size, which panes and the cursor are visible), pacing, voice and render mode.");
3101
+ var openParam = (whenOmitted) => z2.boolean().optional().describe(`Open the finished video in the default player. Defaults to ${whenOmitted}.`);
3102
+ var QUEUED_MESSAGE = "Queued. It renders in the background, even if this client closes; check it with codevideo_get_render_job.";
3103
+ var jobReply = (result) => ({
3104
+ content: [{
3105
+ type: "text",
3106
+ text: JSON.stringify({ ...describeJob(result.job), estimatedSeconds: result.estimatedSeconds, message: QUEUED_MESSAGE }, null, 2)
3107
+ }]
1053
3108
  });
1054
- const server = new McpServer({
3109
+ var errorReply = (prefix, error) => ({
3110
+ isError: true,
3111
+ content: [{ type: "text", text: `${prefix}: ${error instanceof Error ? error.message : String(error)}` }]
3112
+ });
3113
+ function createMcpServer() {
3114
+ const server = new McpServer({
1055
3115
  name: "codevideo-mcp",
1056
- version: version,
1057
- env: {
1058
- // preserve existing environment variables
1059
- ...Object.fromEntries(Object.entries(process.env)
1060
- .filter(([, value]) => value !== undefined)
1061
- .map(([key, value]) => [key, value])),
1062
- MCP_TIMEOUT: String(1200000), // 20 min - needed for video creation waiting
3116
+ version
3117
+ });
3118
+ setupMcpServerTools(server);
3119
+ return server;
3120
+ }
3121
+ function setupMcpServerTools(server) {
3122
+ const tool = server.tool.bind(server);
3123
+ tool(
3124
+ "codevideo_instructions_to_create_a_course",
3125
+ {
3126
+ additionalContext: z2.string()
3127
+ },
3128
+ async ({ additionalContext }) => {
3129
+ return {
3130
+ content: [{
3131
+ type: "text",
3132
+ text: instructionsToCreateACourse(additionalContext)
3133
+ }]
3134
+ };
1063
3135
  }
1064
- });
1065
- // Commenting out prompts and resources that aren't showing up
1066
- // - does Claude just use them internally?
1067
- /*
1068
- // add prompts
1069
- server.prompt(
1070
- "codevideo_create_lesson",
1071
- { additionalContext: z.string() },
1072
- ({ additionalContext }) => ({
1073
- messages: [{
1074
- role: "user",
1075
- content: {
1076
- type: "text",
1077
- text: createLesson(additionalContext)
1078
- }
3136
+ );
3137
+ tool(
3138
+ "codevideo_instructions_to_create_lesson_actions",
3139
+ {
3140
+ additionalContext: z2.string()
3141
+ },
3142
+ async ({ additionalContext }) => {
3143
+ return {
3144
+ content: [{
3145
+ type: "text",
3146
+ text: instructionsToCreateLessonActions(additionalContext)
1079
3147
  }]
1080
- })
1081
- );
1082
-
1083
- // add resources
1084
- server.resource(
1085
- "codevideo_get_action_names",
1086
- "codevideo_get_action_names://codevideo_get_action_names",
1087
- (uri) => ({
1088
- contents: [{
1089
- uri: uri.href,
1090
- text: getActionNames()
3148
+ };
3149
+ }
3150
+ );
3151
+ tool(
3152
+ "codevideo_instructions_to_create_lesson_video",
3153
+ {
3154
+ additionalContext: z2.string()
3155
+ },
3156
+ async ({ additionalContext }) => {
3157
+ return {
3158
+ content: [{
3159
+ type: "text",
3160
+ text: instructionsToCreateLessonVideo(additionalContext)
1091
3161
  }]
1092
- })
1093
- );
1094
-
1095
- server.resource(
1096
- "codevideo_get_example_lesson",
1097
- new ResourceTemplate("codevideo_get_example_lesson://{keyword}", { list: undefined }),
1098
- async (uri, { keyword }) => ({
1099
- contents: [{
1100
- uri: uri.href,
1101
- text: getExampleLesson(keyword)
3162
+ };
3163
+ }
3164
+ );
3165
+ tool(
3166
+ "codevideo_instructions_to_translate_actions",
3167
+ "Generate translation instructions for CodeVideo actions. Pass actions array OR use previously stored actions from codevideo_set_current_actions (recommended workflow).",
3168
+ {
3169
+ actionsToTranslation: z2.array(ActionSchema).optional().describe("Array of CodeVideo actions to translate. Optional if actions were previously stored with codevideo_set_current_actions"),
3170
+ targetLanguage: z2.string().describe("Target language for translation (e.g., 'Spanish', 'French', 'German')")
3171
+ },
3172
+ async ({ actionsToTranslation, targetLanguage }) => {
3173
+ const typedActions = actionsToTranslation;
3174
+ return {
3175
+ content: [{
3176
+ type: "text",
3177
+ text: instructionsToTranslateActions(typedActions, targetLanguage)
1102
3178
  }]
1103
- })
1104
- );
1105
- */
1106
- server.tool("codevideo_instructions_to_create_a_course", {
1107
- additionalContext: z.string()
1108
- }, async ({ additionalContext }) => {
1109
- return {
3179
+ };
3180
+ }
3181
+ );
3182
+ tool(
3183
+ "codevideo_get_empty_lesson_snapshot",
3184
+ {},
3185
+ async () => {
3186
+ return {
1110
3187
  content: [{
1111
- type: "text",
1112
- text: instructionsToCreateACourse(additionalContext)
1113
- }]
1114
- };
1115
- });
1116
- server.tool("codevideo_instructions_to_create_lesson_actions", {
1117
- additionalContext: z.string()
1118
- }, async ({ additionalContext }) => {
1119
- return {
3188
+ type: "text",
3189
+ text: getEmptyLessonSnapshot()
3190
+ }]
3191
+ };
3192
+ }
3193
+ );
3194
+ tool(
3195
+ "codevideo_create_course_with_initial_metadata",
3196
+ {
3197
+ id: z2.string(),
3198
+ name: z2.string(),
3199
+ description: z2.string(),
3200
+ primaryLanguage: z2.string()
3201
+ },
3202
+ async ({ id, name, description, primaryLanguage }) => {
3203
+ return {
1120
3204
  content: [{
1121
- type: "text",
1122
- text: instructionsToCreateLessonActions(additionalContext)
1123
- }]
1124
- };
1125
- });
1126
- server.tool("codevideo_instructions_to_create_lesson_video", {
1127
- additionalContext: z.string()
1128
- }, async ({ additionalContext }) => {
1129
- return {
3205
+ type: "text",
3206
+ text: createCourseWithInitialMetadata(id, name, description, primaryLanguage)
3207
+ }]
3208
+ };
3209
+ }
3210
+ );
3211
+ tool(
3212
+ "codevideo_add_actions_to_lesson",
3213
+ {
3214
+ lesson: LessonSchema,
3215
+ actions: z2.array(ActionSchema)
3216
+ },
3217
+ async ({ lesson, actions }) => {
3218
+ const typedLesson = lesson;
3219
+ const typedActions = actions;
3220
+ return {
1130
3221
  content: [{
1131
- type: "text",
1132
- text: instructionsToCreateLessonVideo(additionalContext)
1133
- }]
1134
- };
1135
- });
1136
- server.tool("codevideo_get_empty_lesson_snapshot", {}, async () => {
1137
- return {
3222
+ type: "text",
3223
+ text: addActionsToLesson(typedLesson, typedActions)
3224
+ }]
3225
+ };
3226
+ }
3227
+ );
3228
+ tool(
3229
+ "codevideo_get_final_lesson_snapshot",
3230
+ "Get the final state snapshot after applying CodeVideo actions. Pass actions array OR use previously stored actions from codevideo_set_current_actions (recommended workflow).",
3231
+ {
3232
+ actions: z2.array(ActionSchema).optional().describe("Array of CodeVideo actions to apply. Optional if actions were previously stored with codevideo_set_current_actions")
3233
+ },
3234
+ async ({ actions }) => {
3235
+ const typedActions = actions;
3236
+ return {
1138
3237
  content: [{
1139
- type: "text",
1140
- text: getEmptyLessonSnapshot()
1141
- }]
1142
- };
1143
- });
1144
- server.tool("codevideo_create_course_with_initial_metadata", {
1145
- id: z.string(),
1146
- name: z.string(),
1147
- description: z.string(),
1148
- primaryLanguage: z.string()
1149
- }, async ({ id, name, description, primaryLanguage }) => {
1150
- return {
3238
+ type: "text",
3239
+ text: getFinalLessonSnapshot(typedActions)
3240
+ }]
3241
+ };
3242
+ }
3243
+ );
3244
+ tool(
3245
+ "codevideo_add_lesson_to_course",
3246
+ {
3247
+ course: CourseSchema,
3248
+ id: z2.string(),
3249
+ name: z2.string(),
3250
+ description: z2.string()
3251
+ },
3252
+ async ({ course, id, name, description }) => {
3253
+ const typedCourse = course;
3254
+ return {
1151
3255
  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 {
3256
+ type: "text",
3257
+ text: addLessonToCourse(typedCourse, id, name, description)
3258
+ }]
3259
+ };
3260
+ }
3261
+ );
3262
+ tool(
3263
+ "codevideo_get_action_names",
3264
+ "Every CodeVideo action, one per line: what it does and what its value takes. Check the rows for the actions you plan to use.",
3265
+ {},
3266
+ async () => {
3267
+ return {
1166
3268
  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 {
3269
+ type: "text",
3270
+ text: getActionNames()
3271
+ }]
3272
+ };
3273
+ }
3274
+ );
3275
+ tool(
3276
+ "codevideo_get_example_actions_array_by_keyword",
3277
+ {
3278
+ keyword: z2.string()
3279
+ },
3280
+ async ({ keyword }) => {
3281
+ return {
1178
3282
  content: [{
1179
- type: "text",
1180
- text: getFinalLessonSnapshot(typedActions)
1181
- }]
1182
- };
1183
- });
1184
- server.tool("codevideo_add_lesson_to_course", {
1185
- course: CourseSchema,
1186
- id: z.string(),
1187
- name: z.string(),
1188
- description: z.string()
1189
- }, async ({ course, id, name, description }) => {
1190
- // Cast the course to ICourse
1191
- const typedCourse = course;
1192
- return {
3283
+ type: "text",
3284
+ text: getExampleActionsArrayByKeyword(keyword)
3285
+ }]
3286
+ };
3287
+ }
3288
+ );
3289
+ tool(
3290
+ "codevideo_get_example_lesson_by_keyword",
3291
+ {
3292
+ keyword: z2.string()
3293
+ },
3294
+ async ({ keyword }) => {
3295
+ return {
1193
3296
  content: [{
1194
- type: "text",
1195
- text: addLessonToCourse(typedCourse, id, name, description)
1196
- }]
1197
- };
1198
- });
1199
- server.tool("codevideo_get_action_names", {}, async () => {
1200
- return {
3297
+ type: "text",
3298
+ text: getExampleLessonByKeyword(keyword)
3299
+ }]
3300
+ };
3301
+ }
3302
+ );
3303
+ tool(
3304
+ "codevideo_get_initial_lesson_state_from_local_repo",
3305
+ "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.",
3306
+ {
3307
+ repoLocation: z2.string().describe("Local file system path to the repository/codebase directory")
3308
+ },
3309
+ async ({ repoLocation }) => {
3310
+ return {
1201
3311
  content: [{
1202
- type: "text",
1203
- text: getActionNames()
1204
- }]
1205
- };
1206
- });
1207
- server.tool("codevideo_get_example_lesson", {
1208
- keyword: z.string()
1209
- }, async ({ keyword }) => {
1210
- return {
3312
+ type: "text",
3313
+ text: await getInitialLessonStateFromLocalCodebase(repoLocation)
3314
+ }]
3315
+ };
3316
+ }
3317
+ );
3318
+ tool(
3319
+ "codevideo_get_initial_lesson_state_from_github_repo",
3320
+ "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.",
3321
+ {
3322
+ repoUrl: z2.string().describe("GitHub repository URL (e.g., https://github.com/user/repo)")
3323
+ },
3324
+ async ({ repoUrl }) => {
3325
+ return {
1211
3326
  content: [{
1212
- type: "text",
1213
- text: getExampleLesson(keyword)
1214
- }]
1215
- };
1216
- });
1217
- server.tool("codevideo_get_initial_lesson_state_from_local_repo", {
1218
- repoLocation: z.string()
1219
- }, async ({ repoLocation }) => {
1220
- return {
3327
+ type: "text",
3328
+ text: await getInitialLessonStateFromGitHubRepo(repoUrl)
3329
+ }]
3330
+ };
3331
+ }
3332
+ );
3333
+ tool(
3334
+ "codevideo_make_markdown_from_actions",
3335
+ "Generate a markdown blog post from CodeVideo actions array. Pass actions array OR use previously stored actions from codevideo_set_current_actions (recommended workflow).",
3336
+ {
3337
+ actions: z2.array(ActionSchema).optional().describe("Array of CodeVideo actions. Optional if actions were previously stored with codevideo_set_current_actions")
3338
+ },
3339
+ async ({ actions }) => {
3340
+ const typedActions = actions;
3341
+ return {
1221
3342
  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 {
3343
+ type: "text",
3344
+ text: makeMarkdownFromActions(typedActions)
3345
+ }]
3346
+ };
3347
+ }
3348
+ );
3349
+ tool(
3350
+ "codevideo_make_markdown_from_lesson",
3351
+ "Generate a markdown blog post from a complete lesson object. Pass a lesson object OR use previously stored lesson from lesson management tools. This extracts the actions from the lesson and generates markdown.",
3352
+ {
3353
+ lesson: LessonSchema.optional().describe("Complete lesson object. Optional if lesson was previously stored with lesson management tools")
3354
+ },
3355
+ async ({ lesson }) => {
3356
+ const typedLesson = lesson;
3357
+ return {
1233
3358
  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 {
3359
+ type: "text",
3360
+ text: makeMarkdownFromLesson(typedLesson)
3361
+ }]
3362
+ };
3363
+ }
3364
+ );
3365
+ tool(
3366
+ "codevideo_make_html_web_page",
3367
+ "Generate an HTML web page from CodeVideo actions. Pass actions array OR use previously stored actions from codevideo_set_current_actions (recommended workflow).",
3368
+ {
3369
+ actions: z2.array(ActionSchema).optional().describe("Array of CodeVideo actions. Optional if actions were previously stored with codevideo_set_current_actions")
3370
+ },
3371
+ async ({ actions }) => {
3372
+ const typedActions = actions;
3373
+ return {
1245
3374
  content: [{
1246
- type: "text",
1247
- text: await makeHTMLWebPage(typedActions)
1248
- }]
1249
- };
1250
- });
1251
- server.tool("codevideo_make_video", "Generate a video from CodeVideo actions. Pass actions array OR use previously stored actions from codevideo_set_current_actions (recommended workflow).", {
1252
- actions: z.array(ActionSchema).optional().describe("Array of CodeVideo actions. Optional if actions were previously stored with codevideo_set_current_actions")
1253
- }, async ({ actions }) => {
1254
- // Cast the actions to IAction if provided
1255
- const typedActions = actions;
1256
- try {
1257
- const result = await makeVideo(typedActions);
1258
- return {
1259
- content: [{
1260
- type: "text",
1261
- text: JSON.stringify({
1262
- outputPath: result.outputPath,
1263
- stdout: result.stdout,
1264
- stderr: result.stderr
1265
- }, null, 2)
1266
- }]
1267
- };
3375
+ type: "text",
3376
+ text: await makeHTMLWebPage(typedActions)
3377
+ }]
3378
+ };
1268
3379
  }
1269
- catch (error) {
1270
- console.error('Error making video:', error);
1271
- // If the error has the VideoResult structure
1272
- if (error && typeof error === 'object' && 'stdout' in error && 'stderr' in error) {
1273
- return {
1274
- content: [{
1275
- type: "text",
1276
- text: `Error creating video: ${error.error?.message || String(error.error)}\nstdout: ${error.stdout}\nstderr: ${error.stderr}`
1277
- }]
1278
- };
1279
- }
1280
- // Fall back to simple error message
1281
- return {
1282
- content: [{
1283
- type: "text",
1284
- text: `Error creating video: ${error?.message || String(error)}`
1285
- }]
1286
- };
3380
+ );
3381
+ tool(
3382
+ "codevideo_make_video_from_actions",
3383
+ "Render a video from CodeVideo actions: pass actions, or omit them to use the ones stored with codevideo_set_current_actions. Queues a background render job and returns its jobId at once; follow it with codevideo_get_render_job.",
3384
+ {
3385
+ actions: z2.array(ActionSchema).optional().describe("Array of CodeVideo actions. Optional if actions were previously stored with codevideo_set_current_actions"),
3386
+ options: RenderOptionsParam,
3387
+ open: openParam(true)
3388
+ },
3389
+ async ({ actions, options, open }) => {
3390
+ try {
3391
+ return jobReply(queueVideoFromActions(actions, options ?? {}, open ?? true));
3392
+ } catch (error) {
3393
+ return errorReply("Couldn't queue the video", error);
3394
+ }
1287
3395
  }
1288
- });
1289
- server.tool("codevideo_validate_actions", {
1290
- actions: z.array(ActionSchema).optional()
1291
- }, async ({ actions }) => {
1292
- // Cast the actions to IAction if provided
1293
- const typedActions = actions;
1294
- return {
3396
+ );
3397
+ tool(
3398
+ "codevideo_make_video_from_lesson",
3399
+ "Render a video from a complete lesson object: pass the lesson, or omit it to use the one stored with codevideo_set_current_lesson. Queues a background render job and returns its jobId at once; follow it with codevideo_get_render_job. For lessons in a stored course, use codevideo_render_lesson_video instead.",
3400
+ {
3401
+ lesson: LessonSchema.optional().describe("Complete lesson object with snapshots. Optional if lesson was previously stored with lesson management tools"),
3402
+ options: RenderOptionsParam,
3403
+ open: openParam(true)
3404
+ },
3405
+ async ({ lesson, options, open }) => {
3406
+ try {
3407
+ return jobReply(queueVideoFromLesson(lesson, options ?? {}, open ?? true));
3408
+ } catch (error) {
3409
+ return errorReply("Couldn't queue the video", error);
3410
+ }
3411
+ }
3412
+ );
3413
+ tool(
3414
+ "codevideo_validate_actions",
3415
+ "Checks a list of actions: unknown names, values of the wrong kind, and actions lessons can't use, each with what to do instead.",
3416
+ {
3417
+ actions: z2.array(ActionSchema).optional()
3418
+ },
3419
+ async ({ actions }) => {
3420
+ const typedActions = actions;
3421
+ return {
1295
3422
  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 {
3423
+ type: "text",
3424
+ text: validateActions(typedActions)
3425
+ }]
3426
+ };
3427
+ }
3428
+ );
3429
+ tool(
3430
+ "codevideo_is_repeatable_action",
3431
+ {
3432
+ action: ActionSchema
3433
+ },
3434
+ async ({ action }) => {
3435
+ const typedAction = action;
3436
+ return {
1307
3437
  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 {
3438
+ type: "text",
3439
+ text: isRepeatable(typedAction)
3440
+ }]
3441
+ };
3442
+ }
3443
+ );
3444
+ tool(
3445
+ "codevideo_critique_video_using_gemini",
3446
+ "Critique a generated video using Google Gemini AI. Pass actions array OR use previously stored actions from codevideo_set_current_actions (recommended workflow).",
3447
+ {
3448
+ videoPath: z2.string().describe("Path to the video file to critique"),
3449
+ actions: z2.array(ActionSchema).optional().describe("Array of CodeVideo actions used to generate the video. Optional if actions were previously stored with codevideo_set_current_actions")
3450
+ },
3451
+ async ({ videoPath, actions }) => {
3452
+ const typedActions = actions;
3453
+ try {
1320
3454
  const critique = await critiqueVideoUsingGemini(videoPath, typedActions);
1321
3455
  return {
1322
- content: [{
1323
- type: "text",
1324
- text: critique
1325
- }]
3456
+ content: [{
3457
+ type: "text",
3458
+ text: critique
3459
+ }]
1326
3460
  };
1327
- }
1328
- catch (error) {
3461
+ } catch (error) {
1329
3462
  const errorMessage = error instanceof Error ? error.message : String(error);
1330
3463
  console.error("Error critiquing video:", errorMessage);
1331
3464
  return {
1332
- content: [{
1333
- type: "text",
1334
- text: `Error critiquing video: ${errorMessage}`
1335
- }]
3465
+ content: [{
3466
+ type: "text",
3467
+ text: `Error critiquing video: ${errorMessage}`
3468
+ }]
1336
3469
  };
3470
+ }
1337
3471
  }
1338
- });
1339
- server.tool("codevideo_set_current_actions", "Store actions in local SQLite database for reuse across multiple tool calls. Once set, other tools can be called without re-specifying actions.", {
1340
- actions: z.array(ActionSchema).describe("Array of CodeVideo actions to store for later use")
1341
- }, async ({ actions }) => {
1342
- // Cast the actions to IAction
1343
- const typedActions = actions;
1344
- return {
3472
+ );
3473
+ tool(
3474
+ "codevideo_set_current_actions",
3475
+ "Legacy single-slot store: keep one actions array for later tool calls, so they can run without re-sending it. Every conversation shares this one slot. For courses or several lessons, use codevideo_create_course and the course tools, which keep work apart by id.",
3476
+ {
3477
+ actions: z2.array(ActionSchema).describe("Array of CodeVideo actions to store for later use")
3478
+ },
3479
+ async ({ actions }) => {
3480
+ const typedActions = actions;
3481
+ return {
1345
3482
  content: [{
1346
- type: "text",
1347
- text: setCurrentActions(typedActions)
1348
- }]
1349
- };
1350
- });
1351
- server.tool("codevideo_get_current_actions", "Retrieve the currently stored actions from local database. Use this to inspect what actions were previously set with codevideo_set_current_actions.", {}, async () => {
1352
- const actions = getCurrentActions();
1353
- if (!actions) {
3483
+ type: "text",
3484
+ text: setCurrentActions(typedActions)
3485
+ }]
3486
+ };
3487
+ }
3488
+ );
3489
+ tool(
3490
+ "codevideo_get_current_actions",
3491
+ "Legacy single-slot store: read back the actions stored with codevideo_set_current_actions (shared by every conversation).",
3492
+ {},
3493
+ async () => {
3494
+ const actions = getCurrentActions();
3495
+ if (!actions) {
1354
3496
  return {
1355
- content: [{
1356
- type: "text",
1357
- text: "No actions found in storage. Use codevideo_set_current_actions first to store actions."
1358
- }]
3497
+ content: [{
3498
+ type: "text",
3499
+ text: "No actions found in storage. Use codevideo_set_current_actions first to store actions."
3500
+ }]
1359
3501
  };
3502
+ }
3503
+ return {
3504
+ content: [{
3505
+ type: "text",
3506
+ text: JSON.stringify(actions, null, 2)
3507
+ }]
3508
+ };
1360
3509
  }
1361
- return {
3510
+ );
3511
+ tool(
3512
+ "codevideo_set_current_lesson",
3513
+ "Legacy single-slot store: keep one lesson object for later tool calls, so they can run without re-sending it. Every conversation shares this one slot. For courses or several lessons, use codevideo_create_course and the course tools, which keep work apart by id.",
3514
+ {
3515
+ lesson: LessonSchema.describe("Complete lesson object to store for later use")
3516
+ },
3517
+ async ({ lesson }) => {
3518
+ const typedLesson = lesson;
3519
+ return {
1362
3520
  content: [{
1363
- type: "text",
1364
- text: JSON.stringify(actions, null, 2)
1365
- }]
1366
- };
1367
- });
1368
- // start the server
1369
- const transport = new StdioServerTransport();
1370
- (async () => {
3521
+ type: "text",
3522
+ text: setCurrentLesson(typedLesson)
3523
+ }]
3524
+ };
3525
+ }
3526
+ );
3527
+ tool(
3528
+ "codevideo_get_current_lesson",
3529
+ "Legacy single-slot store: read back the lesson stored with codevideo_set_current_lesson (shared by every conversation).",
3530
+ {},
3531
+ async () => {
3532
+ const lesson = getCurrentLesson();
3533
+ if (!lesson) {
3534
+ return {
3535
+ content: [{
3536
+ type: "text",
3537
+ text: "No lesson found in storage. Use codevideo_set_current_lesson first to store a lesson."
3538
+ }]
3539
+ };
3540
+ }
3541
+ return {
3542
+ content: [{
3543
+ type: "text",
3544
+ text: JSON.stringify(lesson, null, 2)
3545
+ }]
3546
+ };
3547
+ }
3548
+ );
3549
+ tool(
3550
+ "codevideo_instructions_to_build_course_from_scratch",
3551
+ "Get step-by-step instructions for building a COMPLETE multi-lesson 'build from scratch' course with the course-store tools (snapshots stay server-side). Use this before authoring a full course.",
3552
+ { additionalContext: z2.string().describe("What the course should teach / build") },
3553
+ async ({ additionalContext }) => ({
3554
+ content: [{ type: "text", text: instructionsToBuildCourseFromScratch(additionalContext) }]
3555
+ })
3556
+ );
3557
+ tool(
3558
+ "codevideo_create_course",
3559
+ "Create a new course (server-side, returns a courseId). Snapshots and lesson state live in the MCP, not in your context.",
3560
+ {
3561
+ name: z2.string(),
3562
+ description: z2.string().optional(),
3563
+ primaryLanguage: z2.string().optional(),
3564
+ id: z2.string().optional()
3565
+ },
3566
+ async ({ name, description, primaryLanguage, id }) => ({
3567
+ content: [{ type: "text", text: createCourseTool({ name, description, primaryLanguage, id }) }]
3568
+ })
3569
+ );
3570
+ tool(
3571
+ "codevideo_add_lesson",
3572
+ "Add a lesson to a course. By default it continues from the previous lesson's final state (chained server-side). Set mustCompile:false for slideshow/discussion/half-refactor lessons. The reply includes the previous lesson's name and description, for the recap that opens this lesson.",
3573
+ {
3574
+ courseId: z2.string(),
3575
+ name: z2.string(),
3576
+ description: z2.string().min(1, "describe the lesson in one sentence; the next lesson's recap is written from it"),
3577
+ mustCompile: z2.boolean().optional(),
3578
+ continuesFromPrevious: z2.boolean().optional()
3579
+ },
3580
+ async ({ courseId, name, description, mustCompile, continuesFromPrevious }) => ({
3581
+ content: [{ type: "text", text: addLessonTool(courseId, { name, description, mustCompile, continuesFromPrevious }) }]
3582
+ })
3583
+ );
3584
+ tool(
3585
+ "codevideo_set_lesson_actions",
3586
+ "Store a lesson's actions, replacing any it had. The server recomputes the lesson's final snapshot and re-chains following lessons. Returns only a small ack (no snapshot). To change part of a lesson later, use codevideo_edit_lesson_actions instead of sending every action again.",
3587
+ {
3588
+ courseId: z2.string(),
3589
+ lessonId: z2.string(),
3590
+ actions: z2.array(StrictActionSchema)
3591
+ },
3592
+ async ({ courseId, lessonId, actions }) => {
3593
+ try {
3594
+ return { content: [{ type: "text", text: setLessonActionsTool(courseId, lessonId, actions) }] };
3595
+ } catch (error) {
3596
+ return errorReply("Couldn't store the actions", error);
3597
+ }
3598
+ }
3599
+ );
3600
+ tool(
3601
+ "codevideo_edit_lesson_actions",
3602
+ "Change part of a lesson without re-sending it: each edit removes deleteCount actions at start and inserts the given ones there, like Array.splice. Edits apply in order (each start refers to the actions as the previous edits left them), all or nothing, with one snapshot recompute. Use codevideo_get_lesson_actions to find indices.",
3603
+ {
3604
+ courseId: z2.string(),
3605
+ lessonId: z2.string(),
3606
+ edits: z2.array(z2.object({
3607
+ start: z2.number().int().min(0).describe("Index of the first action to change"),
3608
+ deleteCount: z2.number().int().min(0).describe("How many actions to remove at start (0 to only insert)"),
3609
+ insert: z2.array(StrictActionSchema).default([]).describe("Actions to insert at start")
3610
+ })).min(1)
3611
+ },
3612
+ async ({ courseId, lessonId, edits }) => {
3613
+ try {
3614
+ return { content: [{ type: "text", text: editLessonActionsTool(courseId, lessonId, edits) }] };
3615
+ } catch (error) {
3616
+ return errorReply("Couldn't apply the edits", error);
3617
+ }
3618
+ }
3619
+ );
3620
+ tool(
3621
+ "codevideo_get_lesson_actions",
3622
+ "Read part of a lesson's actions with their indices (default: the first 50), e.g. to find what to change with codevideo_edit_lesson_actions.",
3623
+ {
3624
+ courseId: z2.string(),
3625
+ lessonId: z2.string(),
3626
+ start: z2.number().int().min(0).optional(),
3627
+ limit: z2.number().int().min(1).max(500).optional()
3628
+ },
3629
+ async ({ courseId, lessonId, start, limit }) => ({
3630
+ content: [{ type: "text", text: getLessonActionsTool(courseId, lessonId, start, limit) }]
3631
+ })
3632
+ );
3633
+ tool(
3634
+ "codevideo_get_lesson_file_tree",
3635
+ "List the file paths present at the END of a lesson (cheap; no file contents, no snapshot).",
3636
+ { courseId: z2.string(), lessonId: z2.string() },
3637
+ async ({ courseId, lessonId }) => ({
3638
+ content: [{ type: "text", text: getLessonFileTreeTool(courseId, lessonId) }]
3639
+ })
3640
+ );
3641
+ tool(
3642
+ "codevideo_get_lesson_file",
3643
+ "Get the contents of ONE file at the END of a lesson (cheap targeted read instead of dumping the whole snapshot).",
3644
+ { courseId: z2.string(), lessonId: z2.string(), path: z2.string() },
3645
+ async ({ courseId, lessonId, path: path9 }) => ({
3646
+ content: [{ type: "text", text: getLessonFileTool(courseId, lessonId, path9) }]
3647
+ })
3648
+ );
3649
+ tool(
3650
+ "codevideo_compile_lesson",
3651
+ "Compile the code at the end of a lesson and return only the structured errors. Use this to check a lesson builds before moving on.",
3652
+ { courseId: z2.string(), lessonId: z2.string() },
3653
+ async ({ courseId, lessonId }) => ({
3654
+ content: [{ type: "text", text: await compileLessonTool(courseId, lessonId) }]
3655
+ })
3656
+ );
3657
+ tool(
3658
+ "codevideo_validate_course",
3659
+ "Validate the whole course: per-lesson action validity, snapshot continuity, and compilation (for mustCompile lessons + the final lesson). Returns done:true when the course is complete.",
3660
+ { courseId: z2.string() },
3661
+ async ({ courseId }) => ({
3662
+ content: [{ type: "text", text: await validateCourseTool(courseId) }]
3663
+ })
3664
+ );
3665
+ tool(
3666
+ "codevideo_get_course_status",
3667
+ "Resumable checkpoint view of a course: lessons, action counts, estimated video length per lesson, flags \u2014 without pulling any snapshot.",
3668
+ { courseId: z2.string() },
3669
+ async ({ courseId }) => ({
3670
+ content: [{ type: "text", text: getCourseStatusTool(courseId) }]
3671
+ })
3672
+ );
3673
+ tool(
3674
+ "codevideo_export_course",
3675
+ "Export the full course (ICourse with snapshots) to a JSON file under ~/.codevideo/exports and return its path. The course itself isn't returned, so it doesn't fill your context.",
3676
+ { courseId: z2.string() },
3677
+ async ({ courseId }) => ({
3678
+ content: [{ type: "text", text: exportCourseTool(courseId) }]
3679
+ })
3680
+ );
3681
+ tool(
3682
+ "codevideo_generate_lesson_preview",
3683
+ "Generate a server-side HTML preview of a single lesson (open in a browser to eyeball it). Cheap; no video.",
3684
+ { courseId: z2.string(), lessonId: z2.string() },
3685
+ async ({ courseId, lessonId }) => ({
3686
+ content: [{ type: "text", text: await generateLessonPreviewTool(courseId, lessonId) }]
3687
+ })
3688
+ );
3689
+ tool(
3690
+ "codevideo_generate_course_previews",
3691
+ "Generate HTML previews for every lesson \u2014 GATED: only works once codevideo_validate_course returns done. Use for human review before rendering video.",
3692
+ { courseId: z2.string() },
3693
+ async ({ courseId }) => ({
3694
+ content: [{ type: "text", text: await generateCoursePreviewsTool(courseId) }]
3695
+ })
3696
+ );
3697
+ tool(
3698
+ "codevideo_render_lesson_video",
3699
+ "Render ONE lesson of a stored course to video: queues a background render job and returns its jobId. GATED: refuses until codevideo_validate_course returns done.",
3700
+ { courseId: z2.string(), lessonId: z2.string(), options: RenderOptionsParam, open: openParam(true) },
3701
+ async ({ courseId, lessonId, options, open }) => {
3702
+ try {
3703
+ return { content: [{ type: "text", text: await renderLessonVideoTool(courseId, lessonId, options ?? {}, open ?? true) }] };
3704
+ } catch (error) {
3705
+ return errorReply("Couldn't queue the video", error);
3706
+ }
3707
+ }
3708
+ );
3709
+ tool(
3710
+ "codevideo_render_course_videos",
3711
+ "Render every lesson of a stored course to video: queues one background render job per lesson, run one at a time in lesson order. GATED: refuses until codevideo_validate_course returns done. Run only after a human approves the previews.",
3712
+ { courseId: z2.string(), options: RenderOptionsParam, open: openParam(false) },
3713
+ async ({ courseId, options, open }) => {
3714
+ try {
3715
+ return { content: [{ type: "text", text: await renderCourseVideosTool(courseId, options ?? {}, open ?? false) }] };
3716
+ } catch (error) {
3717
+ return errorReply("Couldn't queue the videos", error);
3718
+ }
3719
+ }
3720
+ );
3721
+ tool(
3722
+ "codevideo_get_render_job",
3723
+ "Status of one render job: queued (with how many jobs are ahead), running (with the latest progress line), succeeded (the video exists at outputPath), failed (with the CLI's error), cancelled or interrupted. Includes the last lines of the CLI's log.",
3724
+ { jobId: z2.string() },
3725
+ async ({ jobId }) => {
3726
+ const job = describeJobById(jobId, true);
3727
+ return job ? { content: [{ type: "text", text: JSON.stringify(job, null, 2) }] } : { isError: true, content: [{ type: "text", text: `No render job ${jobId}.` }] };
3728
+ }
3729
+ );
3730
+ tool(
3731
+ "codevideo_list_render_jobs",
3732
+ "Recent render jobs, newest first (default 20), optionally for one course or in one status.",
3733
+ {
3734
+ courseId: z2.string().optional(),
3735
+ status: z2.enum(["queued", "running", "succeeded", "failed", "cancelled", "interrupted"]).optional(),
3736
+ limit: z2.number().int().min(1).max(100).optional()
3737
+ },
3738
+ async ({ courseId, status, limit }) => ({
3739
+ content: [{ type: "text", text: JSON.stringify(listJobs({ courseId, status, limit }).map((job) => describeJob(job)), null, 2) }]
3740
+ })
3741
+ );
3742
+ tool(
3743
+ "codevideo_cancel_render_job",
3744
+ "Cancel one render job, or every unfinished render job of a course. Queued jobs are dropped; a running one is stopped along with its browser.",
3745
+ { jobId: z2.string().optional(), courseId: z2.string().optional() },
3746
+ async ({ jobId, courseId }) => {
3747
+ if (!jobId === !courseId) {
3748
+ return { isError: true, content: [{ type: "text", text: "Pass exactly one of jobId or courseId." }] };
3749
+ }
3750
+ const { cancelled, stopping } = requestCancel({ jobId, courseId });
3751
+ return {
3752
+ content: [{
3753
+ type: "text",
3754
+ text: JSON.stringify({ cancelled, stopping, message: stopping > 0 ? "Stopping the running job; it shows as cancelled within a few seconds." : "Done." }, null, 2)
3755
+ }]
3756
+ };
3757
+ }
3758
+ );
3759
+ }
3760
+ var invokedPath = process.argv[1];
3761
+ var isDirectExecution = invokedPath !== void 0 && fs9.realpathSync(fileURLToPath(import.meta.url)) === fs9.realpathSync(invokedPath);
3762
+ if (isDirectExecution && process.argv.includes("--render-worker")) {
3763
+ runWorker().then(
3764
+ () => process.exit(0),
3765
+ (error) => {
3766
+ console.error(error);
3767
+ process.exit(1);
3768
+ }
3769
+ );
3770
+ } else if (isDirectExecution) {
3771
+ const server = createMcpServer();
3772
+ const transport = new StdioServerTransport();
3773
+ (async () => {
1371
3774
  await server.connect(transport);
1372
- })();
3775
+ })();
3776
+ }
3777
+ export {
3778
+ createMcpServer
3779
+ };
3780
+ //# sourceMappingURL=index.js.map