@basicsu/courses-mcp
Version:
Interactive programming courses from Basics - MCP server for Cursor
818 lines (766 loc) ⢠43.4 kB
JavaScript
// Phase Zero Tools - Interactive programming courses
import { z } from 'zod';
import { getLocalState, saveLocalState, clearLocalState } from '../state.js';
import { createInstructorResponse } from '../instructor-v2.js';
import { getUserProfile, getUserEnrollments, getPublishedCourses, getCourseByIdentifier, getCourseModules, getCourseLessons, enrollUserInCourse, updateUserProgress, getLessonSteps, getStepById, markStepComplete, getUserStepProgress, getNextStep, getUserPreferences } from '../database-helper.js';
import { calculateHierarchicalProgress } from '../hierarchical-progress.js';
import { getDeviceId, saveDeviceCredentials, getDeviceEmail, getDeviceUserProfileId, clearDeviceCredentials } from '../device-credentials.js';
import { loadConfig, hasValidAuth, clearAuth } from '../config.js';
export const startBasics = {
name: 'startBasics',
description: 'Start or continue learning with Basics. If you are not registered, you will be prompted to register first. Otherwise, it will start at your dashboard or pick up where you last left off. ALWAYS ask the user for their email address if they are not registered. DO NOT assume their credentials, they must confirm their email.',
inputSchema: z.object({
email: z.string().email().optional().describe('Your email address for authentication'),
courseId: z.string().optional().describe('Specific course ID to start (optional)')
}),
handler: async ({ email, courseId }, context) => {
try {
// Check if the user is already authenticated
const deviceId = await getDeviceId();
// If already authenticated and no email provided, show current status
if (deviceId !== null && !email) {
const currentEmail = await getDeviceEmail();
if (currentEmail) {
// If they provided a courseId, continue with current auth
if (courseId) {
// Continue below to start the course
}
else {
return `# Already Logged In
You're currently logged in as: **${currentEmail}**
To switch accounts:
1. Use \`logoutBasics\` to logout
2. Then use \`startBasics({ email: "newemail@example.com" })\` to login with a different account
To check current status: \`getBasicsCourseStatus\`
To continue learning: \`nextBasicsCourseStep\``;
}
}
}
// If they provided an email and are already logged in as someone else
if (deviceId !== null && email) {
const currentEmail = await getDeviceEmail();
if (currentEmail && currentEmail !== email) {
return `# ā ļø Already Logged In as Different User
You're currently logged in as: **${currentEmail}**
But trying to login as: **${email}**
Please logout first: \`logoutBasics\`
Then login with the new account: \`startBasics({ email: "${email}" })\``;
}
}
if (deviceId === null) {
// User is not authenticated
if (!email) {
return `# š Authentication Required
To get started with Basics, I'll need your email address.
If you don't have an account yet, you can register at [basicsu.com/auth/register](https://basicsu.com/auth/register).
Could you please provide your email address?
Example: \`startBasics({ email: "you@example.com" })\``;
}
// Get user profile directly without auth code verification
const userProfile = await getUserProfile(email);
if (!userProfile) {
return `# ā User Not Found
No account found for ${email}. Please register at [basicsu.com/auth/register](https://basicsu.com/auth/register)`;
}
// Save device credentials
await saveDeviceCredentials(userProfile.id, email);
}
// Get user email from device credentials if not provided
const userEmail = email || await getDeviceEmail();
if (!userEmail) {
return 'Authentication error. Please use the `startBasics` tool with your email and auth code.';
}
// Get user's course enrollments with progress
const userCourses = await getUserEnrollments(userEmail);
// Get all published courses
const availableCourses = await getPublishedCourses();
// If courseId provided, start that specific course
if (courseId) {
console.error(`[Course Start] Starting course with ID: ${courseId}`);
const targetCourse = await getCourseByIdentifier(courseId);
console.error(`[Course Start] Found course: ${targetCourse ? targetCourse.title : 'NOT FOUND'}`);
if (!targetCourse) {
return `# ā Course Not Found
The course "${courseId}" was not found.
**Available courses:**
${availableCourses.map(c => `- ${c.title} (${c.slug || c.id})`).join('\n')}
Try again with a valid course ID or slug.`;
}
// Check if user is enrolled
const isEnrolled = userCourses?.some((uc) => uc.course_id === targetCourse.id);
const userProfile = await getUserProfile(userEmail);
if (!isEnrolled && userProfile) {
// Enroll the user
await enrollUserInCourse(userProfile.id, targetCourse.id);
}
// Get the course modules and lessons
console.error(`[Course Start] Fetching modules for course ${targetCourse.id}`);
const modules = await getCourseModules(targetCourse.id);
console.error(`[Course Start] Found ${modules ? modules.length : 0} modules`);
if (!modules || modules.length === 0) {
console.error(`[Course Start] ā ERROR: No modules found for course`);
return `# ā Course Content Not Ready
The course "${targetCourse.title}" doesn't have any modules yet. Please try another course or check back later.`;
}
const firstModule = modules[0];
console.error(`[Course Start] First module: ${firstModule.title} (${firstModule.id})`);
const lessons = await getCourseLessons(firstModule.id);
console.error(`[Course Start] Found ${lessons ? lessons.length : 0} lessons in first module`);
if (!lessons || lessons.length === 0) {
console.error(`[Course Start] ā ERROR: No lessons found in first module`);
return `# ā Course Content Not Ready
The course "${targetCourse.title}" doesn't have any lessons yet. Please try another course or check back later.`;
}
const firstLesson = lessons[0];
console.error(`[Course Start] First lesson: ${firstLesson.title} (${firstLesson.id})`);
// Set up local state for course navigation
const courseState = {
currentCourse: targetCourse.id,
currentModule: firstModule.id,
currentLesson: firstLesson.id,
currentStep: 0,
currentStepId: undefined, // Will be set when they go to first step
userEmail: userEmail
};
console.error(`[Course Start] Saving initial course state`);
await saveLocalState(courseState);
console.error(`[Course Start] Course started successfully: ${targetCourse.title}`);
// Get user preferences for personalized explanations
const userPreferences = userProfile ? await getUserPreferences(userProfile.id) : null;
// Build the RAW lesson content (exactly as created by course author)
const rawLessonContent = `# š ${targetCourse.title} - Started!
## ${firstModule.title}
### ${firstLesson.title}
${firstLesson.content || 'Welcome to this course!'}
**Course Details:**
- Duration: ${targetCourse.estimated_duration || 'Self-paced'}
- Difficulty: ${targetCourse.difficulty_level}
- Tags: ${targetCourse.tags?.join(', ') || 'None'}
**Your Progress Dashboard:**
š [View full course](https://basicsu.com/courses/${targetCourse.slug || targetCourse.id})`;
return createInstructorResponse(rawLessonContent, {
lessonName: firstLesson.title,
stepName: 'Starting',
type: 'course_start',
customSummary: 'Course Navigation is now active! The lesson content above is your starting material. Use `nextBasicsCourseStep` to proceed to the next step.',
userPreferences
});
}
// No courseId provided - show dashboard or ask which course
let response = `# šÆ Your Basics Learning Dashboard
Welcome back, ${userEmail}!
**š [View Your Full Dashboard](https://basicsu.com/dashboard)**
`;
// Show enrolled courses
if (userCourses && userCourses.length > 0) {
response += `## š Your Courses\n\n`;
for (const enrollment of userCourses) {
const progressPercent = Math.round(enrollment.progress * 100);
const statusEmoji = progressPercent >= 100 ? 'ā
' :
progressPercent > 0 ? 'š' : 'ā';
response += `### ${statusEmoji} ${enrollment.title}
- Progress: ${progressPercent}% complete
- Difficulty: ${enrollment.difficulty_level}
- Duration: ${enrollment.estimated_duration || 'Self-paced'}
- **Continue**: \`startBasics({ courseId: "${enrollment.slug || enrollment.course_id}" })\`
`;
}
}
else {
response += `## š No Courses Yet
You haven't enrolled in any courses yet. Check out our available courses below or visit [basicsu.com/courses](https://basicsu.com/courses) to browse all options.
`;
}
// If user has exactly one course, offer to start it
if (userCourses && userCourses.length === 1 && !courseId) {
const onlyCourse = userCourses[0];
response += `\n## š” Quick Start
You have one course in progress. Would you like to continue?
- **Continue "${onlyCourse.title}"**: \`startBasics({ courseId: "${onlyCourse.slug || onlyCourse.course_id}" })\``;
}
response += `\n## š ļø Commands
- Start specific course: \`startBasics({ courseId: "course-name" })\`
- Continue current lesson: \`nextBasicsCourseStep\`
- Check progress: \`getBasicsCourseStatus\`
- Reset progress: \`clearBasicsCourseHistory({ confirm: true })\``;
return response;
}
catch (error) {
return `Error: ${error.message}`;
}
}
};
export const getBasicsCourseStatus = {
name: 'getBasicsCourseStatus',
description: 'Show detailed course progress and web URL',
inputSchema: z.object({}),
handler: async (params, context) => {
try {
// Check if the user is authenticated
const deviceId = await getDeviceId();
if (deviceId === null) {
return 'You need to authenticate first. Please use the `startBasics` tool to authenticate.';
}
// Get user email and profile ID from device credentials
const email = await getDeviceEmail();
const userProfileId = await getDeviceUserProfileId();
if (!email || !userProfileId) {
return 'Authentication error. Please use the `startBasics` tool to re-authenticate.';
}
// Use the stored profile ID
const userProfile = { id: userProfileId };
// Get user's course enrollments with progress
const userCourses = await getUserEnrollments(email);
if (!userCourses || userCourses.length === 0) {
return `# š Course Status
No courses started yet! Use \`startBasics\` to explore available courses.
š **[View Dashboard](https://basicsu.com/dashboard)**`;
}
// Get current state
const state = await getLocalState();
let response = `# š Course Progress Report
**Student**: ${email}
`;
// Show detailed progress for each course
for (const enrollment of userCourses) {
const progressPercent = Math.round(enrollment.progress * 100);
const isCurrent = state.currentCourse === enrollment.course_id;
response += `\n## ${isCurrent ? 'ā¶ļø' : 'š'} ${enrollment.title}
- **Progress**: ${progressPercent}% complete ${progressPercent >= 100 ? 'ā
' : ''}
- **Started**: ${new Date(enrollment.started_at).toLocaleDateString()}
- **Last accessed**: ${new Date(enrollment.last_accessed_at).toLocaleDateString()}
- **Difficulty**: ${enrollment.difficulty_level}
- **Web URL**: [View Course](https://basicsu.com/courses/${enrollment.slug || enrollment.course_id})
`;
if (isCurrent && state.currentLesson && state.currentModule) {
try {
// Get hierarchical progress
const hierarchicalProgress = await calculateHierarchicalProgress(userProfile.id, enrollment.course_id, state.currentModule, state.currentLesson, state.currentStepId);
response += `\n**Currently on**: ${hierarchicalProgress.currentLesson.title}
- **Current Step**: ${hierarchicalProgress.currentStep.index + 1} of ${hierarchicalProgress.currentStep.totalSteps}
- **Lesson Progress**: ${hierarchicalProgress.lessonProgress.currentLessonPercentage}% of "${hierarchicalProgress.currentLesson.title}"
- **Module Progress**: ${hierarchicalProgress.moduleProgress.currentModulePercentage}% of "${hierarchicalProgress.currentModule.title}" (${hierarchicalProgress.currentModule.index + 1} of ${hierarchicalProgress.currentModule.totalModules})
- **Course Progress**: ${hierarchicalProgress.courseProgress.percentage}% overall
**Detailed Progress**:
- **Steps**: ${hierarchicalProgress.stepProgress.completed} of ${hierarchicalProgress.stepProgress.total} completed
- **Lessons**: ${hierarchicalProgress.lessonProgress.completed} of ${hierarchicalProgress.lessonProgress.total} completed
- **Modules**: ${hierarchicalProgress.moduleProgress.completed} of ${hierarchicalProgress.moduleProgress.total} completed
- Continue: \`nextBasicsCourseStep\`
- Jump to different lesson: \`startBasicsCourseLesson({ lessonNumber: X })\`
`;
}
catch (error) {
// Error calculating hierarchical progress
response += `\n**Currently on**: ${state.currentLesson}
- Continue: \`nextBasicsCourseStep\`
`;
}
}
}
response += `\n## š ļø Available Commands
- Continue learning: \`nextBasicsCourseStep\`
- Start a specific course: \`startBasics({ courseId: "course-name" })\`
- Reset all progress: \`clearBasicsCourseHistory({ confirm: true })\`
š **[Full Dashboard](https://basicsu.com/dashboard)**`;
return response;
}
catch (error) {
return `Error getting status: ${error.message}`;
}
}
};
export const nextBasicsCourseStep = {
name: 'nextBasicsCourseStep',
description: 'Move to the next step in the course',
inputSchema: z.object({}),
handler: async (params, context) => {
const startTime = Date.now();
console.error(`[SECURITY] Tool invoked: nextBasicsCourseStep at ${new Date().toISOString()}`);
try {
// Check if the user is authenticated
const deviceId = await getDeviceId();
if (deviceId === null) {
console.error(`[AUTH] ā No device ID found - user not authenticated`);
return 'You need to authenticate first. Please use the `startBasics` tool to authenticate.';
}
// Get user email and profile ID from device credentials
const email = await getDeviceEmail();
const userProfileId = await getDeviceUserProfileId();
console.error(`[AUTH] Authenticated as: ${email ? email.substring(0, 3) + '***' : 'UNKNOWN'}`);
console.error(`[AUTH] Profile ID: ${userProfileId ? userProfileId.substring(0, 8) + '***' : 'UNKNOWN'}`);
if (!email || !userProfileId) {
console.error(`[AUTH] ā Critical: Missing email or profile ID after authentication`);
console.error(`[AUTH] Email: ${email || 'NULL'}, Profile ID: ${userProfileId || 'NULL'}`);
return `It appears that there is a persistent issue with your user profile not being found, even after re-authenticating with your email. This may be a problem with the course platform's authentication or session handling.
Here are a few steps you can try:
1. Make sure you are logged in to the platform at basicsu.com with your email address.
2. If you are already logged in, try logging out and logging back in.
3. Then use \`startBasics({ email: "your-email@example.com" })\` to re-authenticate.
If the issue persists, please contact support at support@basicsu.com.`;
}
const state = await getLocalState();
if (!state.currentCourse || !state.currentModule || !state.currentLesson) {
return "No course in progress. Use `startBasics` to begin a course first!";
}
// Use the stored profile ID instead of fetching the profile
const userProfile = { id: userProfileId };
// Get user preferences for personalized explanations
const userPreferences = await getUserPreferences(userProfileId);
// Get all modules and lessons for the course to calculate total progress
const allModules = await getCourseModules(state.currentCourse);
let totalLessons = 0;
let completedLessons = 0;
let currentModuleIndex = -1;
// Calculate total course progress
for (let i = 0; i < allModules.length; i++) {
const module = allModules[i];
const moduleLessons = await getCourseLessons(module.id);
totalLessons += moduleLessons.length;
if (module.id === state.currentModule) {
currentModuleIndex = i;
// Count completed lessons in current module
const currentLessonIndex = moduleLessons.findIndex(l => l.id === state.currentLesson);
completedLessons += currentLessonIndex;
}
else if (i < currentModuleIndex) {
// All lessons in previous modules are completed
completedLessons += moduleLessons.length;
}
}
// Get current lesson content
const lessons = await getCourseLessons(state.currentModule);
const currentLessonIndex = lessons.findIndex(l => l.id === state.currentLesson);
const currentLesson = lessons[currentLessonIndex];
if (!currentLesson) {
return 'Error: Current lesson not found. Please restart the course.';
}
// Get lesson steps from database
console.error(`[Course Content] Fetching steps for lesson: ${state.currentLesson}`);
const lessonSteps = await getLessonSteps(state.currentLesson);
console.error(`[Course Content] Found ${lessonSteps.length} steps in lesson`);
if (lessonSteps.length === 0) {
console.error(`[Course Content] ā ERROR: No steps found for lesson ${state.currentLesson}`);
return 'Error: No steps found for this lesson. Please contact support.';
}
// Get the next step
let currentStep = null;
let nextStep = null;
if (state.currentStepId) {
// We have a current step, get the next one
console.error(`[Course Content] Current step ID: ${state.currentStepId}`);
currentStep = await getStepById(state.currentStepId);
console.error(`[Course Content] Getting next step after current...`);
nextStep = await getNextStep(userProfile.id, state.currentLesson, state.currentStepId);
console.error(`[Course Content] Next step: ${nextStep ? nextStep.id : 'none (end of lesson)'}`);
}
else {
// First time in this lesson, get the first step
console.error(`[Course Content] First time in lesson, getting first step`);
nextStep = lessonSteps[0];
console.error(`[Course Content] First step ID: ${nextStep.id}`);
}
// Check if we need to move to next lesson
if (!nextStep) {
// Mark the last step of current lesson as complete
if (state.currentStepId) {
console.error(`[Progress Tracking] End of lesson - marking final step complete`);
console.error(`[Progress Tracking] User: ${email}, Step: ${state.currentStepId}`);
const markResult = await markStepComplete(userProfile.id, state.currentStepId);
if (markResult) {
console.error(`[Progress Tracking] ā
Final step marked complete`);
}
else {
console.error(`[Progress Tracking] ā Failed to mark final step complete`);
}
}
// Check if we're at the last lesson of the last module
const isLastModule = currentModuleIndex === allModules.length - 1;
const isLastLessonInModule = currentLessonIndex === lessons.length - 1;
if (isLastModule && isLastLessonInModule) {
// Course completed!
await updateUserProgress(userProfile.id, state.currentCourse, 1.0);
const course = await getCourseByIdentifier(state.currentCourse);
// Get other enrolled courses
const allEnrollments = await getUserEnrollments(email);
const otherCourses = allEnrollments.filter(e => e.course_id !== state.currentCourse && e.progress < 1);
const completedCourses = allEnrollments.filter(e => e.course_id !== state.currentCourse && e.progress >= 1);
// Get available courses for discovery
const allCourses = await getPublishedCourses();
const unenrolledCourses = allCourses.filter(c => !allEnrollments.some(e => e.course_id === c.id)).slice(0, 3); // Show top 3 suggestions
let nextStepsContent = `# š Congratulations! Course Completed!
You've successfully completed **${course?.title || 'this course'}**!
**Your Achievement:**
- ā
All ${totalLessons} lessons completed
- ā
100% progress achieved
- ā
Certificate available at [basicsu.com/certificates](https://basicsu.com/certificates)
`;
// Show other enrolled courses first
if (otherCourses.length > 0) {
nextStepsContent += `## š Continue Your Learning Journey
**Your Other Active Courses:**
`;
for (const enrollment of otherCourses) {
const progress = Math.round(enrollment.progress * 100);
nextStepsContent += `- **${enrollment.title}** (${progress}% complete)
\`startBasics({ courseId: "${enrollment.slug || enrollment.course_id}" })\`
`;
}
}
// Show course discovery
if (unenrolledCourses.length > 0) {
nextStepsContent += `
## š Discover New Courses
`;
for (const course of unenrolledCourses) {
nextStepsContent += `- **${course.title}** - ${course.description?.substring(0, 60)}...
\`startBasics({ courseId: "${course.slug || course.id}" })\`
`;
}
}
nextStepsContent += `
## š ļø Commands
- View full dashboard: \`startBasics\`
- Check all progress: \`getBasicsCourseStatus\`
Keep learning and growing! š`;
// Clear the local state since course is completed
await clearLocalState();
return nextStepsContent;
}
// Move to next lesson or module
if (currentLessonIndex < lessons.length - 1) {
// Next lesson in current module
const nextLesson = lessons[currentLessonIndex + 1];
console.error(`[Course Content] Moving to next lesson: ${nextLesson.title} (${nextLesson.id})`);
state.currentLesson = nextLesson.id;
state.currentStep = 0;
state.currentStepId = undefined; // Reset step ID for new lesson
await saveLocalState(state);
// Update user progress based on total course progress
const newProgress = (completedLessons + currentLessonIndex + 1) / totalLessons;
await updateUserProgress(userProfile.id, state.currentCourse, newProgress);
// Check if approaching end of course
const lessonsRemaining = totalLessons - (completedLessons + currentLessonIndex + 1);
let progressNote = `**Progress**: ${Math.round(newProgress * 100)}% of course completed`;
if (lessonsRemaining <= 2) {
progressNote += ` (${lessonsRemaining} lesson${lessonsRemaining === 1 ? '' : 's'} remaining!)`;
}
const rawLessonContent = `# ⨠New Lesson: ${nextLesson.title}
${nextLesson.content || 'Welcome to this lesson!'}
${progressNote}`;
console.error(`[Course Content] New lesson loaded: ${nextLesson.title}`);
console.error(`[Course Content] Lesson content length: ${nextLesson.content ? nextLesson.content.length : 0} characters`);
console.error(`[Course Content] Returning lesson transition response`);
return createInstructorResponse(rawLessonContent, {
lessonName: nextLesson.title,
stepName: 'Starting',
type: 'lesson_complete',
customSummary: 'You\'ve moved to a new lesson! The content above is your new lesson material. Use `nextBasicsCourseStep` to continue learning.',
userPreferences
});
}
else if (currentModuleIndex < allModules.length - 1) {
// Move to next module
const nextModule = allModules[currentModuleIndex + 1];
const nextModuleLessons = await getCourseLessons(nextModule.id);
if (nextModuleLessons.length > 0) {
const nextLesson = nextModuleLessons[0];
state.currentModule = nextModule.id;
state.currentLesson = nextLesson.id;
state.currentStep = 0;
state.currentStepId = undefined; // Reset step ID for new lesson
await saveLocalState(state);
// Update progress
const newProgress = (completedLessons + lessons.length) / totalLessons;
await updateUserProgress(userProfile.id, state.currentCourse, newProgress);
const rawLessonContent = `# šÆ New Module: ${nextModule.title}
${nextModule.description || ''}
## ${nextLesson.title}
${nextLesson.content || 'Welcome to this lesson!'}
**Progress**: ${Math.round(newProgress * 100)}% of course completed
**Module**: ${currentModuleIndex + 2} of ${allModules.length}`;
console.error(`[Course Content] New module loaded: ${nextModule.title} (${nextModule.id})`);
console.error(`[Course Content] First lesson of module: ${nextLesson.title}`);
console.error(`[Course Content] Module ${currentModuleIndex + 2}/${allModules.length}`);
return createInstructorResponse(rawLessonContent, {
lessonName: nextLesson.title,
stepName: 'Starting',
type: 'module_start',
customSummary: 'You\'ve started a new module! The content above introduces the new material. Use `nextBasicsCourseStep` to continue learning.',
userPreferences
});
}
}
}
// Mark the previous step as complete when moving to next step
if (state.currentStepId && currentStep) {
console.error(`[Progress Tracking] Marking step complete for user: ${email}`);
console.error(`[Progress Tracking] Profile ID: ${userProfile.id}`);
console.error(`[Progress Tracking] Step ID: ${state.currentStepId}`);
const markResult = await markStepComplete(userProfile.id, state.currentStepId);
if (markResult) {
console.error(`[Progress Tracking] ā
Step marked complete successfully`);
// Get updated progress
const progressData = await getUserStepProgress(userProfile.id, state.currentCourse);
console.error(`[Progress Tracking] Course Progress: ${progressData.completedSteps}/${progressData.totalSteps} steps (${Math.round(progressData.progress * 100)}%)`);
}
else {
console.error(`[Progress Tracking] ā Failed to mark step complete`);
}
}
// We have a next step! Update state and display it
console.error(`[Course Content] Loading step content for: ${nextStep.id}`);
console.error(`[Course Content] Step title: ${nextStep.title}`);
console.error(`[Course Content] Step type: ${nextStep.step_type}`);
state.currentStepId = nextStep.id;
state.currentStep = lessonSteps.findIndex(s => s.id === nextStep.id); // Keep backward compatibility
await saveLocalState(state);
console.error(`[Course Content] State saved, step index: ${state.currentStep + 1}/${lessonSteps.length}`);
// Get hierarchical progress for this course
console.error(`[Course Content] Calculating hierarchical progress...`);
const hierarchicalProgress = await calculateHierarchicalProgress(userProfile.id, state.currentCourse, state.currentModule, state.currentLesson, nextStep.id);
console.error(`[Course Content] Course progress: ${hierarchicalProgress.courseProgress.percentage}%`);
// Find step number within lesson
const stepIndex = lessonSteps.findIndex(s => s.id === nextStep.id);
const stepNumber = stepIndex + 1;
// Build the RAW lesson content (exactly as created by course author)
const rawLessonContent = `# ${currentLesson.title}
## ${nextStep.title}
${nextStep.content}`;
console.error(`[Course Content] Step content length: ${nextStep.content ? nextStep.content.length : 0} characters`);
if (!nextStep.content || nextStep.content.trim() === '') {
console.error(`[Course Content] ā ļø WARNING: Step has no content!`);
}
// For quiz steps, provide context about previous steps
const instructorContext = {
lessonName: currentLesson.title,
stepName: `Step ${stepNumber}`,
type: 'step_navigation',
stepType: nextStep.step_type,
progress: {
stepNumber,
totalSteps: lessonSteps.length,
lessonProgress: `${hierarchicalProgress.lessonProgress.currentLessonPercentage}% of "${hierarchicalProgress.currentLesson.title}" (${hierarchicalProgress.lessonProgress.completed}/${hierarchicalProgress.lessonProgress.total} lessons completed)`,
moduleProgress: `${hierarchicalProgress.moduleProgress.currentModulePercentage}% of "${hierarchicalProgress.currentModule.title}" (${hierarchicalProgress.moduleProgress.completed}/${hierarchicalProgress.moduleProgress.total} modules completed)`,
courseProgress: `${hierarchicalProgress.courseProgress.percentage}% overall`
},
userPreferences
};
// If it's a quiz step, add previous steps context
if (nextStep.step_type === 'quiz') {
instructorContext.quizStepIds = nextStep.quiz_step_ids || [];
instructorContext.previousSteps = lessonSteps.slice(0, stepIndex);
}
// Use smart instructor wrapper that preserves content integrity
console.error(`[Course Content] Creating instructor response for step ${stepNumber}/${lessonSteps.length}`);
const response = createInstructorResponse(rawLessonContent, instructorContext);
console.error(`[Course Content] Response created, length: ${response.length} characters`);
const endTime = Date.now();
console.error(`[PERFORMANCE] nextBasicsCourseStep completed in ${endTime - startTime}ms`);
console.error(`[SECURITY] Operation completed successfully at ${new Date().toISOString()}`);
return response;
}
catch (error) {
console.error(`[ERROR] nextBasicsCourseStep failed: ${error.message}`);
console.error(`[ERROR] Stack trace: ${error.stack}`);
console.error(`[SECURITY] Failed operation logged at ${new Date().toISOString()}`);
return `Error moving to next step: ${error.message}`;
}
}
};
export const startBasicsCourseLesson = {
name: 'startBasicsCourseLesson',
description: 'Jump to a specific lesson',
inputSchema: z.object({
lessonNumber: z.number()
}),
handler: async ({ lessonNumber }, context) => {
try {
// Check if the user is authenticated
const deviceId = await getDeviceId();
if (deviceId === null) {
return 'You need to authenticate first. Please use the `startBasics` tool to authenticate.';
}
// Get user email from device credentials
const email = await getDeviceEmail();
if (!email) {
return 'Authentication error. Please use the `startBasics` tool to re-authenticate.';
}
const state = await getLocalState();
if (!state.currentCourse || !state.currentModule) {
return "No course in progress. Use `startBasics` to begin a course first!";
}
// Get all lessons for current module
const lessons = await getCourseLessons(state.currentModule);
if (!lessons || lessons.length === 0) {
return "No lessons found in current module.";
}
// Validate lesson number
if (lessonNumber < 1 || lessonNumber > lessons.length) {
return `Invalid lesson number. Please choose between 1 and ${lessons.length}.
Available lessons:
${lessons.map((l, i) => `${i + 1}. ${l.title}`).join('\n')}`;
}
// Jump to the specified lesson
const targetLesson = lessons[lessonNumber - 1];
state.currentLesson = targetLesson.id;
state.currentStep = 0;
await saveLocalState(state);
// Get user profile ID for preferences
const userProfileId = await getDeviceUserProfileId();
// Get user preferences for personalized explanations
const userPreferences = userProfileId ? await getUserPreferences(userProfileId) : null;
// Get course for context
const course = await getCourseByIdentifier(state.currentCourse);
const rawLessonContent = `# Jumped to Lesson ${lessonNumber}: ${targetLesson.title}
${targetLesson.content || 'Welcome to this lesson!'}
**Course**: ${course?.title || 'Current Course'}
**Lesson**: ${lessonNumber} of ${lessons.length}`;
return createInstructorResponse(rawLessonContent, {
lessonName: targetLesson.title,
stepName: 'Starting',
type: 'lesson_jump',
customSummary: 'You\'ve jumped to a specific lesson. The content above is your lesson material. Use `nextBasicsCourseStep` to continue learning.',
userPreferences
});
}
catch (error) {
return `Error jumping to lesson: ${error.message}`;
}
}
};
export const completeCurrentStep = {
name: 'completeCurrentStep',
description: 'Mark the current step as complete (useful for the last step in a course)',
inputSchema: z.object({}),
handler: async (params, context) => {
try {
const deviceId = await getDeviceId();
if (!deviceId) {
return 'You need to authenticate first. Please use the `startBasics` tool to authenticate.';
}
const email = await getDeviceEmail();
const userProfileId = await getDeviceUserProfileId();
if (!email || !userProfileId) {
return 'Authentication error. Please use the `startBasics` tool to re-authenticate.';
}
const userProfile = { id: userProfileId };
const state = await getLocalState();
if (!state.currentCourse || !state.currentStepId) {
return 'No active step to complete. Use `startBasics` to begin a course.';
}
// Mark current step as complete
await markStepComplete(userProfile.id, state.currentStepId);
// Check if this was the last step in the course
const courseModules = await getCourseModules(state.currentCourse);
if (courseModules && courseModules.length > 0) {
const lastModule = courseModules[courseModules.length - 1];
const lastModuleLessons = await getCourseLessons(lastModule.id);
if (lastModuleLessons && lastModuleLessons.length > 0) {
const lastLesson = lastModuleLessons[lastModuleLessons.length - 1];
const lastLessonSteps = await getLessonSteps(lastLesson.id);
if (lastLessonSteps && lastLessonSteps.length > 0) {
const lastStep = lastLessonSteps[lastLessonSteps.length - 1];
if (state.currentStepId === lastStep.id) {
// This was the last step - mark course as complete
await updateUserProgress(userProfile.id, state.currentCourse, 1.0);
return `# š Course Completed!
Congratulations! You've completed the entire course!
**Next Steps:**
- View your certificate: [basicsu.com/certificates](https://basicsu.com/certificates)
- Browse more courses: \`startBasics\`
- Check your progress: \`getBasicsCourseStatus\``;
}
}
}
}
return `ā
Step completed! Use \`nextBasicsCourseStep\` to continue.`;
}
catch (error) {
return `Error: ${error.message}`;
}
}
};
export const clearBasicsCourseHistory = {
name: 'clearBasicsCourseHistory',
description: 'Reset all course progress',
inputSchema: z.object({
confirm: z.boolean()
}),
handler: async ({ confirm }, context) => {
try {
// Check if the user is authenticated
const deviceId = await getDeviceId();
if (deviceId === null) {
return 'You need to authenticate first. Please use the `startBasics` tool to authenticate.';
}
// Get user email from device credentials
const email = await getDeviceEmail();
if (!email) {
return 'Authentication error. Please use the `startBasics` tool to re-authenticate.';
}
if (!confirm) {
return "Are you sure? This will reset all progress. Use clearBasicsCourseHistory({ confirm: true }) to proceed.";
}
await clearLocalState();
// Note: We're not clearing database progress, just local navigation state
// To fully reset, we would need to update all enrollments to 0% progress
return "⨠Course navigation has been reset. Use `startBasics` to begin fresh!";
}
catch (error) {
return `Error clearing history: ${error.message}`;
}
}
};
export const logoutBasics = {
name: 'logoutBasics',
description: 'Logout from Basics and clear stored credentials',
inputSchema: z.object({}),
handler: async (params, context) => {
try {
// Get the current email before logging out for the message
const currentEmail = await getDeviceEmail();
// Clear the authentication
clearAuth();
// Clear device credentials
await clearDeviceCredentials();
// Also clear any local state
await clearLocalState();
const message = currentEmail
? `ā
Successfully logged out from Basics (${currentEmail}). Use \`startBasics\` to login with a different account.`
: "ā
Successfully logged out from Basics. Use `startBasics` to login with a different account.";
return message;
}
catch (error) {
return `Error logging out: ${error.message}`;
}
}
};
export const getBasicsAuthStatus = {
name: 'getBasicsAuthStatus',
description: 'Check current authentication status and logged-in user',
inputSchema: z.object({}),
handler: async (params, context) => {
try {
if (!hasValidAuth()) {
return "Not authenticated. Use `startBasics` to login.";
}
const config = loadConfig();
return `Authenticated as: ${config.userEmail}`;
}
catch (error) {
return `Error checking auth status: ${error.message}`;
}
}
};
// Export all tools as an array for easy registration
export const phaseZeroTools = [
startBasics,
getBasicsCourseStatus,
nextBasicsCourseStep,
startBasicsCourseLesson,
completeCurrentStep,
clearBasicsCourseHistory,
logoutBasics,
getBasicsAuthStatus
];
//# sourceMappingURL=phase-zero-tools.js.map