import { exec, spawn } from 'child_process'; import { promises as fs } from 'fs'; import * as https from 'https'; import * as http from 'http'; import * as crypto from 'crypto'; import * as path from 'path'; import { Plugin } from 'vite'; // ─── Types ─────────────────────────────────────────────────────────────────── interface ExecResult { success: boolean; stdout: string; stderr: string; output: string; error: string; exitCode: number; executionTime: number; durationMs: number; } interface AIThread { id: string; userId: string; title: string; createdAt: string; updatedAt: string; } interface AIMessage { id: string; threadId: string; role: 'user' | 'assistant' | 'system'; content: string; createdAt: string; } // ─── In-memory AI Teacher Store (persisted to JSON file) ────────────────────── const DATA_FILE = path.join(process.cwd(), '.ryp-runner', 'ai-threads.json'); let threadsStore: Map = new Map(); let messagesStore: Map = new Map(); // threadId -> messages async function loadAIData() { try { await fs.mkdir(path.dirname(DATA_FILE), { recursive: true }); const raw = await fs.readFile(DATA_FILE, 'utf-8'); const data = JSON.parse(raw); threadsStore = new Map(Object.entries(data.threads || {})); messagesStore = new Map(Object.entries(data.messages || {})); } catch { // Fresh start } } async function saveAIData() { try { await fs.mkdir(path.dirname(DATA_FILE), { recursive: true }); const data = { threads: Object.fromEntries(threadsStore), messages: Object.fromEntries(messagesStore), }; await fs.writeFile(DATA_FILE, JSON.stringify(data, null, 2)); } catch (e) { // Ignore } } // ─── JWT decode (no verification — dev only) ────────────────────────────────── function decodeJwtUserId(authHeader: string | undefined): string { if (!authHeader) return ''; const token = authHeader.replace(/^Bearer\s+/i, '').trim(); try { const parts = token.split('.'); if (parts.length !== 3) return ''; const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8')); return payload.sub || payload.userId || payload.id || ''; } catch { return ''; } } // ─── HTTP helper for Groq API ───────────────────────────────────────────────── function groqRequest(apiKey: string, payload: object): Promise { return new Promise((resolve, reject) => { const body = JSON.stringify(payload); const options: https.RequestOptions = { hostname: 'api.groq.com', port: 443, path: '/openai/v1/chat/completions', method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, 'Content-Length': Buffer.byteLength(body), }, }; const req = https.request(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { resolve({ status: res.statusCode, body: JSON.parse(data) }); } catch { reject(new Error('Invalid JSON from Groq')); } }); }); req.on('error', reject); req.setTimeout(60000, () => { req.destroy(new Error('Groq request timeout')); }); req.write(body); req.end(); }); } // ─── DuckDuckGo Web Search ──────────────────────────────────────────────────── function stripHtmlTags(s: string): string { return s.replace(/<[^>]*>/g, '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").trim(); } function searchDuckDuckGo(query: string): Promise { return new Promise((resolve) => { const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`; const options: https.RequestOptions = { hostname: 'html.duckduckgo.com', path: `/html/?q=${encodeURIComponent(query)}`, method: 'GET', headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36', 'Accept': 'text/html', 'Accept-Language': 'en-US,en;q=0.9', }, timeout: 10000, }; const req = https.request(options, (res) => { let html = ''; res.on('data', (c) => { html += c; }); res.on('end', () => { const snippets: string[] = []; const titles: string[] = []; // Extract snippets const snipParts = html.split('class="result__snippet"'); for (let i = 1; i < snipParts.length && snippets.length < 4; i++) { const start = snipParts[i].indexOf('>'); if (start === -1) continue; const rest = snipParts[i].slice(start + 1); let end = rest.indexOf(''); if (end === -1) end = rest.indexOf(''); if (end < 0 || end > 600) continue; const snippet = stripHtmlTags(rest.slice(0, end)); if (snippet.length > 30) snippets.push(snippet); } // Extract titles const titleParts = html.split('class="result__title"'); for (let i = 1; i < titleParts.length && titles.length < 4; i++) { const start = titleParts[i].indexOf('>'); if (start === -1) continue; const rest = titleParts[i].slice(start + 1); const end = rest.indexOf(' 200) continue; const t = stripHtmlTags(rest.slice(0, end)); if (t.length > 5) titles.push(t); } if (snippets.length === 0) { resolve(titles.length > 0 ? `Search found: ${titles.join(', ')}` : `No results for: ${query}`); return; } let out = `Web search results for "${query}":\n\n`; snippets.forEach((s, i) => { if (i < titles.length) out += `**${titles[i]}**\n`; out += `${s}\n\n`; }); resolve(out); }); }); req.on('error', () => resolve(`Search failed for: ${query}`)); req.on('timeout', () => { req.destroy(); resolve(`Search timed out for: ${query}`); }); req.end(); }); } // ─── AI Teacher System Prompt ───────────────────────────────────────────────── const AI_SYSTEM_PROMPT = `You are RYP AI Teacher — a smart, patient, and encouraging tutor for college students preparing for placements, coding interviews, and exams. ## YOUR IDENTITY You are an expert in: - Data structures & algorithms - Programming (Python, Java, C++, JavaScript, Go, etc.) - Computer science fundamentals (OS, DBMS, Networking, OOP) - Aptitude & reasoning - Resume writing, HR interviews, and placement preparation - System design basics ## HOW TO RESPOND 1. Explain clearly, step by step, with examples. 2. Use Markdown formatting always: - ### Headings for sections - **Bold** for key terms - Numbered lists for steps - Bullet points for features/comparisons - Fenced code blocks with language name for ALL code samples 3. Be encouraging and motivating — like a great college professor. 4. For coding questions: explain the approach, then show code with comments. 5. When grading tests or quizzes, verify the correct option first. Never say an answer is correct unless the user's selected option exactly matches the verified correct option. ## TOOL USAGE — VERY IMPORTANT You have access to a web_search tool. YOU MUST USE web_search when: - The student asks about current job market, placements, companies, or salaries - The student asks for latest documentation, APIs, or framework versions - The student asks about recent events, news, or time-sensitive information - You are unsure if your knowledge is up-to-date - The question involves facts that change over time (prices, statistics, rankings) Do NOT apologize for using tools. Just use them silently and incorporate results into your answer naturally. ## SCOPE You answer questions related to education, study, coding, computer science, aptitude, placements, interviews, and career development. - If the student greets you, greet them warmly and invite a study, coding, placement, or career question. - If the student's text is unclear, random, or incomplete, ask one focused clarifying question instead of guessing. - If the student asks a clearly off-topic question, briefly say you can help with study, coding, CS, aptitude, placements, interviews, and career prep, then ask them to send a related question. - Never repeat an old refusal just because it appears in the chat history. Respond to the latest student message.`; const AI_TUTOR_GREETING_RESPONSE = "Hi! I'm RYP AI Tutor. Ask me anything about coding, CS subjects, aptitude, placements, interviews, or career prep."; const AI_TUTOR_THANKS_RESPONSE = "You're welcome. Send your next study, coding, placement, or interview question whenever you are ready."; const AI_TUTOR_CLARIFICATION_RESPONSE = "I could not understand that clearly. Please ask a clear study, coding, placement, or career question, and I will help step by step."; function normalizeAITeacherText(message: string): string { return message .trim() .toLowerCase() .replace(/^[\s.,!?;:"'`()\[\]{}]+|[\s.,!?;:"'`()\[\]{}]+$/g, '') .split(/\s+/) .filter(Boolean) .join(' '); } function aiTeacherAlphaNumToken(message: string): string { return message.toLowerCase().replace(/[^a-z0-9]/g, ''); } function isAITeacherGreeting(message: string): boolean { return new Set([ 'hi', 'hii', 'hiii', 'hello', 'hey', 'heyy', 'hey there', 'hello there', 'good morning', 'good afternoon', 'good evening', ]).has(message); } function isAITeacherThanks(message: string): boolean { return new Set(['thanks', 'thank you', 'thankyou', 'ok', 'okay', 'cool', 'got it']).has(message); } function countAITeacherVowels(value: string): number { return [...value].filter((ch) => 'aeiou'.includes(ch)).length; } function isKnownAITeacherSingleWord(token: string): boolean { return new Set([ 'ai', 'api', 'aptitude', 'array', 'aws', 'bfs', 'binary', 'career', 'class', 'cloud', 'code', 'coding', 'css', 'data', 'dbms', 'dfs', 'dsa', 'git', 'golang', 'graph', 'html', 'interview', 'java', 'javascript', 'leetcode', 'linux', 'loops', 'mongodb', 'networking', 'node', 'oop', 'oops', 'os', 'placement', 'python', 'queue', 'react', 'recursion', 'resume', 'roadmap', 'sql', 'stack', 'sorting', 'system', 'tree', ]).has(token); } function isLikelyUnclearAITeacherInput(message: string): boolean { const fields = message.split(/\s+/).filter(Boolean); const token = aiTeacherAlphaNumToken(message); if (!token) return true; if (fields.length === 1) { if (isKnownAITeacherSingleWord(token)) return false; if (token.length <= 5) return true; if (countAITeacherVowels(token) === 0 && token.length >= 4) return true; } return fields.length <= 2 && token.length <= 4; } function aiTeacherQuickResponse(message: string, hasAttachments: boolean): string | null { if (hasAttachments) return null; const normalized = normalizeAITeacherText(message); if (!normalized) return null; if (isAITeacherGreeting(normalized)) return AI_TUTOR_GREETING_RESPONSE; if (isAITeacherThanks(normalized)) return AI_TUTOR_THANKS_RESPONSE; if (isLikelyUnclearAITeacherInput(normalized)) return AI_TUTOR_CLARIFICATION_RESPONSE; return null; } interface AITeacherAnswerSubmission { answers: Map; canonical: string; } interface AITeacherGradeItem { questionNumber: number; selectedOption: string; correctOption: string; correctAnswer: string; explanation: string; } interface AITeacherGradeResult { items: AITeacherGradeItem[]; } function parseAITeacherAnswerSubmission(message: string): AITeacherAnswerSubmission | null { const normalized = message.trim().toLowerCase(); if (!normalized) return null; const answerPattern = /(?:^|[\s,;|])(?:q(?:uestion)?\s*)?([0-9]{1,2})\s*[\.\):\-]?\s*([a-e])(?=\b|$)/gim; const matches = [...normalized.matchAll(answerPattern)]; if (matches.length < 2) return null; const answers = new Map(); for (const match of matches) { const questionNumber = Number(match[1]); if (!Number.isFinite(questionNumber) || questionNumber <= 0 || questionNumber > 100) continue; answers.set(questionNumber, (match[2] || '').toLowerCase()); } if (answers.size < 2) return null; const leftover = normalized .replace(answerPattern, ' ') .replace(/^[\s,;|/_-]+|[\s,;|/_-]+$/g, '') .trim(); if (leftover && !isAllowedAITeacherAnswerSubmissionPrefix(leftover)) return null; const canonical = [...answers.entries()] .sort(([a], [b]) => a - b) .map(([questionNumber, answer]) => `${questionNumber}.${answer}`) .join('\n'); return { answers, canonical }; } function isAllowedAITeacherAnswerSubmissionPrefix(value: string): boolean { const allowedWords = new Set([ 'answer', 'answers', 'ans', 'my', 'here', 'are', 'is', 'the', 'test', 'quiz', 'submit', 'submitting', 'submitted', 'choice', 'choices', 'option', 'options', ]); return value .split(/\s+/) .map((field) => field.replace(/^[\s.,!?;:"'`()\[\]{}]+|[\s.,!?;:"'`()\[\]{}]+$/g, '')) .filter(Boolean) .every((field) => allowedWords.has(field)); } function looksLikeAITeacherTest(content: string): boolean { const questionMatches = content.match(/(?:^|\n)\s*(?:#{1,4}\s*)?(?:q(?:uestion)?\s*)?([0-9]{1,2})[\).\:\-]?\s+/gim) || []; const optionMatches = content.match(/(?:^|\n)\s*(?:[-*]\s*)?([a-e])[\).\:\-]\s+\S+/gim) || []; return questionMatches.length >= 2 && optionMatches.length >= 4; } function findRecentAITeacherTest(messages: AIMessage[]): string | null { for (let index = messages.length - 1; index >= 0; index -= 1) { const message = messages[index]; if (message.role === 'assistant' && looksLikeAITeacherTest(message.content)) { return message.content; } } return null; } function buildAITeacherUngradableAnswerResponse(): string { return 'I found your answer choices, but I need the test questions in this chat to grade them correctly. Please send the test questions with options, then send answers like `1.a 2.b 3.c`.'; } function normalizeAITeacherOption(value: string): string { const match = (value || '').trim().toLowerCase().match(/[a-e]/); return match?.[0] || ''; } function extractAITeacherJSON(value: string): string { let text = (value || '').trim(); if (text.startsWith('```')) { text = text.replace(/^```json\s*/i, '').replace(/^```\s*/i, '').replace(/```$/i, ''); } return text.trim(); } async function gradeAITeacherAnswerSubmission( groqApiKey: string, testContent: string, submission: AITeacherAnswerSubmission, ): Promise { const questionNumbers = [...submission.answers.keys()].sort((a, b) => a - b); const result = await groqRequest(groqApiKey, { model: 'llama-3.3-70b-versatile', messages: [ { role: 'system', content: `You are a strict quiz answer checker. Return JSON only. Rules: - Use only the provided test questions and options. - Re-solve each question before selecting the correct option. - Do not trust the student's claimed correctness. - selectedOption and correctOption must be lowercase letters a-e. - If a question cannot be verified from the test text, set correctOption to "" and explain what is missing. - Return exactly: {"items":[{"questionNumber":1,"selectedOption":"a","correctOption":"b","correctAnswer":"option text","explanation":"short reason"}]}`, }, { role: 'user', content: `Test questions: ${testContent} Student answers, already normalized to lowercase: ${submission.canonical} Grade only these question numbers: ${questionNumbers.join(', ')}`, }, ], temperature: 0, max_tokens: 1800, response_format: { type: 'json_object' }, }); if (result.status < 200 || result.status >= 300) { throw new Error(`Groq grading error: ${result.status}`); } const content = result.body?.choices?.[0]?.message?.content || ''; const parsed = JSON.parse(extractAITeacherJSON(content)) as AITeacherGradeResult; return renderAITeacherGradeFeedback(submission, parsed); } function renderAITeacherGradeFeedback(submission: AITeacherAnswerSubmission, result: AITeacherGradeResult): string { const itemsByQuestion = new Map(); for (const item of result.items || []) { if (!item?.questionNumber) continue; itemsByQuestion.set(item.questionNumber, { ...item, selectedOption: normalizeAITeacherOption(item.selectedOption), correctOption: normalizeAITeacherOption(item.correctOption), }); } const questionNumbers = [...submission.answers.keys()].sort((a, b) => a - b); let score = 0; let gradable = 0; const lines: string[] = [ '### Test Feedback', '', '| Question | Your answer | Correct answer | Result |', '|---|---:|---:|---|', ]; for (const questionNumber of questionNumbers) { const selected = normalizeAITeacherOption(submission.answers.get(questionNumber) || ''); const item = itemsByQuestion.get(questionNumber); const correct = normalizeAITeacherOption(item?.correctOption || ''); let resultText = 'Needs review'; if (correct) { gradable += 1; if (selected === correct) { score += 1; resultText = 'Correct'; } else { resultText = 'Incorrect'; } } lines.push(`| ${questionNumber} | ${selected.toUpperCase()} | ${correct.toUpperCase()} | ${resultText} |`); } lines.push(''); if (gradable > 0) lines.push(`**Score:** ${score}/${gradable}`, ''); lines.push('### Review', ''); for (const questionNumber of questionNumbers) { const selected = normalizeAITeacherOption(submission.answers.get(questionNumber) || ''); const item = itemsByQuestion.get(questionNumber); const correct = normalizeAITeacherOption(item?.correctOption || ''); if (!item || !correct) { lines.push(`${questionNumber}. **Question ${questionNumber}:** I could not verify this one from the test text. Please resend the question if you want me to check it.`); continue; } const explanation = item.explanation?.trim() || 'Checked by comparing your selected option with the verified correct option.'; if (selected === correct) { lines.push(`${questionNumber}. **Question ${questionNumber}:** Correct. Your answer **${selected.toUpperCase()}** matches the verified answer. ${explanation}`); } else { const answerText = item.correctAnswer?.trim() ? ` (${item.correctAnswer.trim()})` : ''; lines.push(`${questionNumber}. **Question ${questionNumber}:** Incorrect. You answered **${selected.toUpperCase()}**; the correct answer is **${correct.toUpperCase()}**${answerText}. ${explanation}`); } } return lines.join('\n').trim(); } const AI_TOOLS = [ { type: 'function', function: { name: 'web_search', description: 'Search the web for up-to-date information. Use this for current events, job market, latest docs, prices, or any time-sensitive query.', parameters: { type: 'object', properties: { query: { type: 'string', description: 'The search query' }, }, required: ['query'], }, }, }, ]; // ─── Agentic AI Teacher handler ─────────────────────────────────────────────── async function runAITeacher( threadId: string, userMessage: string, attachments: any[], groqApiKey: string ): Promise { // Get last 30 messages for context const threadMessages = (messagesStore.get(threadId) || []).slice(-30); const answerSubmission = parseAITeacherAnswerSubmission(userMessage); if (answerSubmission) { const testContent = findRecentAITeacherTest(threadMessages); if (!testContent) return buildAITeacherUngradableAnswerResponse(); try { return await gradeAITeacherAnswerSubmission(groqApiKey, testContent, answerSubmission); } catch (error) { console.error('[AI Teacher] grading error:', error); return buildAITeacherUngradableAnswerResponse(); } } const quickResponse = aiTeacherQuickResponse(userMessage, attachments?.length > 0); if (quickResponse) return quickResponse; // Build attachment context let userContent = userMessage.trim(); if (attachments?.length > 0) { const attCtx = attachments .filter(a => a.name) .map(a => { let s = `\n- ${a.name}`; if (a.type) s += ` (${a.type})`; if (a.textExcerpt) s += `\n Content:\n${a.textExcerpt.slice(0, 3500)}`; return s; }) .join(''); if (attCtx) userContent += `\n\n[Attached Files]:${attCtx}`; } const msgs: any[] = [ { role: 'system', content: AI_SYSTEM_PROMPT }, ...threadMessages.map(m => ({ role: m.role, content: m.content })), { role: 'user', content: userContent }, ]; // Agentic loop — max 3 iterations let finalResponse = ''; for (let iter = 0; iter < 3; iter++) { const toolChoice = iter === 2 ? 'none' : 'auto'; const result = await groqRequest(groqApiKey, { model: 'llama-3.3-70b-versatile', messages: msgs, temperature: 0.65, max_tokens: 2048, tools: AI_TOOLS, tool_choice: toolChoice, }); if (result.status < 200 || result.status >= 300) { console.error('[AI Teacher] Groq error:', result.status, JSON.stringify(result.body)); throw new Error(`Groq API error: ${result.status}`); } const choice = result.body?.choices?.[0]?.message; if (!choice) throw new Error('No response from Groq'); const msgToAppend: any = { role: choice.role }; if (choice.content) msgToAppend.content = choice.content; if (choice.tool_calls?.length > 0) msgToAppend.tool_calls = choice.tool_calls; msgs.push(msgToAppend); if (choice.tool_calls?.length > 0) { // Execute tools for (const toolCall of choice.tool_calls) { if (toolCall.function?.name === 'web_search') { let args: any = {}; try { args = JSON.parse(toolCall.function.arguments || '{}'); } catch { } const searchResult = await searchDuckDuckGo(args.query || ''); msgs.push({ role: 'tool', tool_call_id: toolCall.id, name: 'web_search', content: searchResult, }); } else { msgs.push({ role: 'tool', tool_call_id: toolCall.id, name: toolCall.function?.name, content: 'Tool not supported.', }); } } // Continue loop to get final answer with tool results } else { finalResponse = choice.content || ''; break; } } return finalResponse || 'I encountered an error formulating a response. Please try again.'; } // ─── Read request body helper ───────────────────────────────────────────────── function readBody(req: http.IncomingMessage): Promise { return new Promise((resolve) => { let body = ''; req.on('data', (chunk) => { body += chunk.toString(); }); req.on('end', () => resolve(body)); req.on('error', () => resolve('')); }); } function jsonOk(res: http.ServerResponse, data: any) { res.setHeader('Content-Type', 'application/json'); res.statusCode = 200; res.end(JSON.stringify(data)); } function jsonErr(res: http.ServerResponse, status: number, msg: string) { res.setHeader('Content-Type', 'application/json'); res.statusCode = status; res.end(JSON.stringify({ error: msg })); } // ─── AI Teacher Vite Plugin ─────────────────────────────────────────────────── export function aiTeacherPlugin(): Plugin { return { name: 'ai-teacher', async configureServer(server) { await loadAIData(); console.log('[AI Teacher] In-process middleware active (bypasses WDAC)'); const groqKey = process.env.GROQ_API_KEY || process.env.VITE_GROQ_API_KEY || ''; // Intercept all /api/ai/* requests BEFORE they hit the proxy server.middlewares.use(async (req: http.IncomingMessage, res: http.ServerResponse, next: () => void) => { const url = req.url || ''; // Only handle /api/ai/* routes if (!url.startsWith('/api/ai/')) return next(); const userId = decodeJwtUserId(req.headers['authorization'] as string); // GET /api/ai/threads if (url === '/api/ai/threads' && req.method === 'GET') { const userThreads = [...threadsStore.values()] .filter(t => t.userId === userId) .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); return jsonOk(res, { threads: userThreads }); } // POST /api/ai/threads/new if (url === '/api/ai/threads/new' && req.method === 'POST') { const body = await readBody(req); let title = 'New Chat'; try { title = JSON.parse(body).title || title; } catch { } const now = new Date().toISOString(); const thread: AIThread = { id: crypto.randomUUID(), userId, title, createdAt: now, updatedAt: now, }; threadsStore.set(thread.id, thread); await saveAIData(); return jsonOk(res, { thread }); } // GET /api/ai/threads/messages?threadId=xxx if (url.startsWith('/api/ai/threads/messages') && req.method === 'GET') { const threadId = new URL(url, 'http://localhost').searchParams.get('threadId') || ''; const thread = threadsStore.get(threadId); if (!thread || thread.userId !== userId) return jsonErr(res, 403, 'Thread not found'); const msgs = messagesStore.get(threadId) || []; return jsonOk(res, { messages: msgs }); } // DELETE /api/ai/threads/delete?threadId=xxx if (url.startsWith('/api/ai/threads/delete') && (req.method === 'DELETE' || req.method === 'POST')) { let threadId = new URL(url, 'http://localhost').searchParams.get('threadId') || ''; if (!threadId) { const body = await readBody(req); try { threadId = JSON.parse(body).threadId || ''; } catch { } } const thread = threadsStore.get(threadId); if (!thread || thread.userId !== userId) return jsonErr(res, 403, 'Thread not found'); threadsStore.delete(threadId); messagesStore.delete(threadId); await saveAIData(); return jsonOk(res, { status: 'deleted' }); } // POST /api/ai/teacher if (url === '/api/ai/teacher' && req.method === 'POST') { if (!groqKey) return jsonErr(res, 500, 'GROQ_API_KEY is not configured in .env'); const body = await readBody(req); let payload: any = {}; try { payload = JSON.parse(body); } catch { return jsonErr(res, 400, 'Invalid JSON'); } const { threadId, message, attachments = [] } = payload; if (!threadId || !message?.trim()) return jsonErr(res, 400, 'threadId and message are required'); const thread = threadsStore.get(threadId); if (!thread || thread.userId !== userId) return jsonErr(res, 403, 'Thread not found'); const now = new Date().toISOString(); // Save user message const userMsg: AIMessage = { id: crypto.randomUUID(), threadId, role: 'user', content: message.trim(), createdAt: now, }; const threadMsgs = messagesStore.get(threadId) || []; threadMsgs.push(userMsg); messagesStore.set(threadId, threadMsgs); // Auto-title thread from first message if (threadMsgs.length === 1) { const titleText = message.trim(); thread.title = titleText.length > 30 ? titleText.slice(0, 30) + '...' : titleText; } thread.updatedAt = now; threadsStore.set(threadId, thread); try { const aiResponse = await runAITeacher(threadId, message, attachments, groqKey); const aiMsg: AIMessage = { id: crypto.randomUUID(), threadId, role: 'assistant', content: aiResponse, createdAt: new Date().toISOString(), }; threadMsgs.push(aiMsg); messagesStore.set(threadId, threadMsgs); await saveAIData(); return jsonOk(res, { thread, userMessage: userMsg, assistantMessage: aiMsg, message: aiMsg }); } catch (err: any) { console.error('[AI Teacher] Error:', err.message); return jsonErr(res, 500, err.message || 'AI Teacher error'); } } // Unknown /api/ai/* route — let Go handle it (shouldn't happen) return next(); }); }, }; } // ─── Code Executor Plugin ───────────────────────────────────────────────────── export function localExecutorPlugin(): Plugin { return { name: 'local-executor', configureServer(server) { server.middlewares.use('/api/execute', async (req, res, next) => { if (req.method !== 'POST') { return next(); } let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', async () => { try { const data = JSON.parse(body); const { language, code, input } = data; if (!language || !code) { res.statusCode = 400; res.end(JSON.stringify({ error: 'Language and code are required' })); return; } const result = await executeLocally(language.toLowerCase(), code, input || ''); res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(result)); } catch (err: any) { res.statusCode = 500; res.end(JSON.stringify({ error: err.message || 'Execution failed' })); } }); }); } }; } async function executeLocally(language: string, code: string, stdin: string): Promise { // Use Wandbox API for compiled languages to bypass Windows Device Guard blocks on EXEs if (['c', 'cpp', 'c++', 'go', 'golang'].includes(language)) { const start = Date.now(); try { let compiler = ''; if (language === 'c') compiler = 'gcc-head-c'; else if (language === 'cpp' || language === 'c++') compiler = 'gcc-head'; else if (language === 'go' || language === 'golang') compiler = 'go-head'; const response = await fetch('https://wandbox.org/api/compile.json', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ compiler: compiler, code: code, stdin: stdin || '' }) }); if (!response.ok) { throw new Error(`Wandbox API error: ${response.statusText}`); } const result: any = await response.json(); const elapsed = Date.now() - start; const stderrStr = result.compiler_error || result.program_error || ''; const stdoutStr = result.program_output || result.compiler_message || ''; const success = result.status === "0"; return { success, stdout: stdoutStr, stderr: stderrStr, output: stdoutStr, error: stderrStr, exitCode: success ? 0 : 1, executionTime: elapsed, durationMs: elapsed, }; } catch (err: any) { return { success: false, stdout: '', stderr: err.message, output: '', error: `Remote Compilation Error:\n${err.message}`, exitCode: 1, executionTime: Date.now() - start, durationMs: Date.now() - start, }; } } // Local execution for interpreted/VM languages (Python, JS, Java) const workDir = path.join(process.cwd(), '.ryp-runner', `run-${Date.now()}`); await fs.mkdir(workDir, { recursive: true }); let sourceFile = ''; let cmd = ''; let args: string[] = []; try { switch (language) { case 'python': case 'python3': case 'py': sourceFile = 'main.py'; cmd = 'python'; args = ['main.py']; break; case 'javascript': case 'js': case 'node': sourceFile = 'main.js'; cmd = 'node'; args = ['main.js']; break; case 'java': sourceFile = 'Main.java'; await fs.writeFile(path.join(workDir, sourceFile), code); await new Promise((resolve, reject) => { exec('javac Main.java', { cwd: workDir }, (error, stdout, stderr) => { if (error) reject(new Error(stderr || stdout || error.message)); resolve(null); }); }); cmd = 'java'; args = ['-cp', '.', 'Main']; break; default: throw new Error(`Unsupported language: ${language}`); } if (sourceFile && language !== 'java') { await fs.writeFile(path.join(workDir, sourceFile), code); } const start = Date.now(); return await new Promise((resolve) => { const child = spawn(cmd, args, { cwd: workDir, shell: process.platform === 'win32' }); let stdoutStr = ''; let stderrStr = ''; let isDone = false; const timeout = setTimeout(() => { if (!isDone) { child.kill(); resolve({ success: false, stdout: stdoutStr, stderr: 'Time Limit Exceeded (>10s)', output: stdoutStr, error: 'Time Limit Exceeded (>10s)', exitCode: -1, executionTime: Date.now() - start, durationMs: Date.now() - start, }); } }, 10000); child.stdout.on('data', (data) => { stdoutStr += data.toString(); }); child.stderr.on('data', (data) => { stderrStr += data.toString(); }); if (stdin) { child.stdin.write(stdin); child.stdin.end(); } child.on('close', (code) => { isDone = true; clearTimeout(timeout); const elapsed = Date.now() - start; if (stderrStr.includes('Application Control policy') || stderrStr.includes('Device Guard')) { stderrStr = 'Execution blocked by Windows Application Control Policy.\n' + stderrStr; } resolve({ success: code === 0, stdout: stdoutStr, stderr: stderrStr, output: stdoutStr, error: stderrStr, exitCode: code ?? -1, executionTime: elapsed, durationMs: elapsed, }); }); child.on('error', (err: any) => { isDone = true; clearTimeout(timeout); const elapsed = Date.now() - start; resolve({ success: false, stdout: stdoutStr, stderr: err.message, output: stdoutStr, error: err.message, exitCode: -1, executionTime: elapsed, durationMs: elapsed, }); }); }); } catch (err: any) { return { success: false, stdout: '', stderr: err.message, output: '', error: `Compilation/Setup Error:\n${err.message}`, exitCode: 1, executionTime: 0, durationMs: 0, }; } finally { setTimeout(() => { fs.rm(workDir, { recursive: true, force: true }).catch(() => { }); }, 1000); } }