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