Reubencf commited on
Commit
e9d7b34
·
1 Parent(s): 467789d

TESTING SESSIONS AND QUIZES

Browse files
IMPLEMENTATION_GUIDE.md ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Reuben OS - Stateless Prefix Strategy Implementation Guide
2
+
3
+ ## Overview
4
+ This implementation uses a simple "Stateless Prefix Strategy" to handle session isolation and file management without complex session state management. All files are stored in `/tmp` with a `{SessionID}_{Filename}` pattern.
5
+
6
+ ## Architecture Components
7
+
8
+ ### 1. API Route (`pages/api/mcp-handler.js`)
9
+ - **Location**: `/pages/api/mcp-handler.js`
10
+ - **Purpose**: Handles all file operations with session prefix logic
11
+ - **Key Features**:
12
+ - Validates session IDs (alphanumeric + hyphens/underscores only)
13
+ - Prefixes all files with `{sessionId}_` when saving to `/tmp`
14
+ - Strips prefixes when returning files to frontend
15
+ - Supports CORS for cross-origin requests
16
+
17
+ **Endpoints**:
18
+ - `POST /api/mcp-handler` - Save/delete files, deploy quizzes
19
+ - `GET /api/mcp-handler?sessionId=...` - Retrieve files for a session
20
+
21
+ ### 2. MCP Server (`mcp-server.js`)
22
+ - **Location**: `/mcp-server.js`
23
+ - **Purpose**: Bridge between Claude Desktop and Reuben OS
24
+ - **Tools**:
25
+ 1. `manage_files` - Save, retrieve, delete, or clear files
26
+ 2. `deploy_quiz` - Deploy interactive quizzes
27
+
28
+ ### 3. Frontend Components (`frontend-fetch-example.js`)
29
+ - **Location**: `/frontend-fetch-example.js`
30
+ - **Components**:
31
+ - `SessionManager` - Main component for file management
32
+ - `FileItem` - Individual file display
33
+ - `FlutterAppViewer` - Flutter/Dart file viewer
34
+ - `LaTeXViewer` - LaTeX document viewer
35
+
36
+ ## Complete Workflow
37
+
38
+ ### Step 1: User Opens Reuben OS
39
+ 1. Frontend generates a session ID: `session_{timestamp}_{random}`
40
+ 2. Session ID is displayed in the UI
41
+ 3. Frontend starts polling `/api/mcp-handler?sessionId=...` every 5 seconds
42
+
43
+ ### Step 2: User Copies Session ID
44
+ 1. User clicks "Copy ID" button in the UI
45
+ 2. Session ID is copied to clipboard
46
+
47
+ ### Step 3: User Provides Session ID to Claude
48
+ ```
49
+ User: "My session ID is session_1234567_abc123"
50
+ Claude: "Got it! I'll use this session ID for all file operations."
51
+ ```
52
+
53
+ ### Step 4: Claude Saves Files
54
+ Claude uses the `manage_files` tool:
55
+ ```javascript
56
+ {
57
+ sessionId: "session_1234567_abc123",
58
+ action: "save",
59
+ fileName: "main.dart",
60
+ content: "// Flutter code here..."
61
+ }
62
+ ```
63
+
64
+ Backend saves as: `/tmp/session_1234567_abc123_main.dart`
65
+
66
+ ### Step 5: Frontend Retrieves Files
67
+ Frontend polls: `GET /api/mcp-handler?sessionId=session_1234567_abc123`
68
+
69
+ API response:
70
+ ```javascript
71
+ {
72
+ success: true,
73
+ sessionId: "session_1234567_abc123",
74
+ files: [
75
+ {
76
+ name: "main.dart", // Prefix stripped!
77
+ size: 1234,
78
+ content: "// Flutter code here...",
79
+ isQuiz: false
80
+ }
81
+ ],
82
+ count: 1
83
+ }
84
+ ```
85
+
86
+ ### Step 6: Quiz Deployment
87
+ Claude deploys a quiz:
88
+ ```javascript
89
+ {
90
+ sessionId: "session_1234567_abc123",
91
+ quizData: {
92
+ title: "JavaScript Quiz",
93
+ questions: [...]
94
+ }
95
+ }
96
+ ```
97
+
98
+ Backend saves as: `/tmp/session_1234567_abc123_quiz.json`
99
+ Frontend detects `quiz.json` and shows "Launch Quiz" button.
100
+
101
+ ## Testing Instructions
102
+
103
+ ### 1. Start the Next.js Server
104
+ ```bash
105
+ npm run dev
106
+ # Server runs on http://localhost:3000
107
+ ```
108
+
109
+ ### 2. Test API Directly
110
+ ```bash
111
+ # Save a file
112
+ curl -X POST http://localhost:3000/api/mcp-handler \
113
+ -H "Content-Type: application/json" \
114
+ -d '{
115
+ "sessionId": "test123",
116
+ "action": "save_file",
117
+ "fileName": "test.txt",
118
+ "content": "Hello World"
119
+ }'
120
+
121
+ # Retrieve files
122
+ curl "http://localhost:3000/api/mcp-handler?sessionId=test123"
123
+ ```
124
+
125
+ ### 3. Configure MCP Server in Claude Desktop
126
+ Add to Claude Desktop settings (`claude_desktop_config.json`):
127
+ ```json
128
+ {
129
+ "mcpServers": {
130
+ "reubenos": {
131
+ "command": "node",
132
+ "args": ["path/to/mcp-server.js"],
133
+ "env": {
134
+ "REUBENOS_URL": "http://localhost:3000"
135
+ }
136
+ }
137
+ }
138
+ }
139
+ ```
140
+
141
+ ### 4. Test with Claude
142
+ 1. Open Claude Desktop
143
+ 2. Tell Claude your session ID
144
+ 3. Ask Claude to save a Flutter app:
145
+ ```
146
+ "Save a Flutter counter app to my session: session_test123"
147
+ ```
148
+ 4. Check the frontend - file should appear immediately
149
+
150
+ ## File Types Supported
151
+
152
+ - **Flutter/Dart**: `.dart` files - Can be opened in Zapp
153
+ - **LaTeX**: `.tex` files - Can be edited in LaTeX Studio
154
+ - **Quizzes**: `quiz.json` - Launches Quiz App
155
+ - **Any text file**: `.js`, `.py`, `.txt`, etc.
156
+
157
+ ## Security Considerations
158
+
159
+ 1. **Session ID Validation**: Only alphanumeric + hyphens/underscores
160
+ 2. **Filename Sanitization**: Prevents path traversal attacks
161
+ 3. **File Size Limits**: 1MB limit for content retrieval
162
+ 4. **Temporary Storage**: Files in `/tmp` are ephemeral
163
+ 5. **No Authentication**: Sessions are isolated by ID only
164
+
165
+ ## Deployment on Hugging Face Spaces
166
+
167
+ 1. **Environment**: Hugging Face provides `/tmp` for temporary storage
168
+ 2. **Persistence**: `/tmp` is cleared on container restart
169
+ 3. **Public Files**: Use `./public/uploads` for permanent storage
170
+ 4. **CORS**: API allows all origins (adjust for production)
171
+
172
+ ## Troubleshooting
173
+
174
+ ### Files Not Appearing
175
+ - Check session ID matches exactly
176
+ - Verify API is accessible at configured URL
177
+ - Check browser console for errors
178
+ - Ensure `/tmp` directory exists and is writable
179
+
180
+ ### MCP Connection Issues
181
+ - Verify `REUBENOS_URL` environment variable
182
+ - Check Claude Desktop has MCP server configured
183
+ - Look at MCP server console output for errors
184
+
185
+ ### Quiz Not Launching
186
+ - Ensure quiz.json has correct structure
187
+ - Check frontend detects `isQuiz: true` flag
188
+ - Verify Quiz App route exists
189
+
190
+ ## Example Session
191
+
192
+ 1. **Frontend**: Generates `session_1732255800000_xyz789`
193
+ 2. **User**: Copies ID, gives to Claude
194
+ 3. **Claude**: Saves `main.dart` using manage_files tool
195
+ 4. **Backend**: Stores as `/tmp/session_1732255800000_xyz789_main.dart`
196
+ 5. **Frontend**: Polls and displays "main.dart" (prefix stripped)
197
+ 6. **User**: Opens file in Zapp runner
198
+ 7. **Claude**: Deploys quiz using deploy_quiz tool
199
+ 8. **Frontend**: Shows quiz alert, launches Quiz App
200
+
201
+ ## Summary
202
+
203
+ This implementation provides:
204
+ - Simple, stateless session management
205
+ - Automatic file isolation by session
206
+ - No complex state tracking
207
+ - Easy to debug and maintain
208
+ - Compatible with Hugging Face Spaces constraints
209
+ - Supports multiple file types and applications
210
+
211
+ The prefix strategy ensures users never see the technical details - they only see clean filenames while the system maintains perfect isolation behind the scenes.
README.md CHANGED
@@ -41,6 +41,16 @@ npm run dev
41
  # Open http://localhost:3000
42
  ```
43
 
 
 
 
 
 
 
 
 
 
 
44
  ### Deploy to Hugging Face Spaces
45
 
46
  This project is ready for deployment on [Hugging Face Spaces](https://huggingface.co/spaces):
 
41
  # Open http://localhost:3000
42
  ```
43
 
44
+ ### Configuration
45
+
46
+ To enable Gemini AI features, create a `.env.local` file in the root directory:
47
+
48
+ ```bash
49
+ GEMINI_API_KEY=your_gemini_api_key_here
50
+ ```
51
+
52
+ This key is used for the Gemini Chat and other AI features. The application uses the `gemini-1.5-flash` model by default.
53
+
54
  ### Deploy to Hugging Face Spaces
55
 
56
  This project is ready for deployment on [Hugging Face Spaces](https://huggingface.co/spaces):
frontend-fetch-example.js ADDED
@@ -0,0 +1,462 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // frontend-fetch-example.js
2
+ // Frontend code snippet for Reuben OS to fetch and display session files
3
+
4
+ // React Component Example for Session Management
5
+ import React, { useState, useEffect, useCallback } from 'react';
6
+
7
+ // Configuration
8
+ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
9
+ const POLL_INTERVAL = 5000; // Poll every 5 seconds
10
+
11
+ // ===== SESSION MANAGER COMPONENT =====
12
+ export function SessionManager() {
13
+ const [sessionId, setSessionId] = useState('');
14
+ const [files, setFiles] = useState([]);
15
+ const [loading, setLoading] = useState(false);
16
+ const [error, setError] = useState(null);
17
+ const [quizDetected, setQuizDetected] = useState(false);
18
+
19
+ // Generate a new session ID on component mount
20
+ useEffect(() => {
21
+ const generateSessionId = () => {
22
+ const timestamp = Date.now();
23
+ const random = Math.random().toString(36).substring(2, 9);
24
+ return `session_${timestamp}_${random}`;
25
+ };
26
+
27
+ // Check if session ID exists in localStorage, otherwise generate new one
28
+ const storedSessionId = localStorage.getItem('reubenOSSessionId');
29
+ if (storedSessionId) {
30
+ setSessionId(storedSessionId);
31
+ } else {
32
+ const newSessionId = generateSessionId();
33
+ setSessionId(newSessionId);
34
+ localStorage.setItem('reubenOSSessionId', newSessionId);
35
+ }
36
+ }, []);
37
+
38
+ // Fetch files for the current session
39
+ const fetchFiles = useCallback(async () => {
40
+ if (!sessionId) return;
41
+
42
+ setLoading(true);
43
+ setError(null);
44
+
45
+ try {
46
+ const response = await fetch(`${API_URL}/api/mcp-handler?sessionId=${sessionId}`);
47
+
48
+ if (!response.ok) {
49
+ throw new Error(`HTTP error! status: ${response.status}`);
50
+ }
51
+
52
+ const data = await response.json();
53
+
54
+ if (data.success) {
55
+ setFiles(data.files || []);
56
+
57
+ // Check if quiz.json exists
58
+ const hasQuiz = data.files.some(file => file.name === 'quiz.json');
59
+ setQuizDetected(hasQuiz);
60
+ } else {
61
+ setError(data.error || 'Failed to fetch files');
62
+ }
63
+ } catch (err) {
64
+ console.error('Error fetching files:', err);
65
+ setError(err.message);
66
+ } finally {
67
+ setLoading(false);
68
+ }
69
+ }, [sessionId]);
70
+
71
+ // Set up polling
72
+ useEffect(() => {
73
+ if (!sessionId) return;
74
+
75
+ // Initial fetch
76
+ fetchFiles();
77
+
78
+ // Set up polling interval
79
+ const interval = setInterval(fetchFiles, POLL_INTERVAL);
80
+
81
+ return () => clearInterval(interval);
82
+ }, [sessionId, fetchFiles]);
83
+
84
+ // Handle manual refresh
85
+ const handleRefresh = () => {
86
+ fetchFiles();
87
+ };
88
+
89
+ // Copy session ID to clipboard
90
+ const copySessionId = () => {
91
+ navigator.clipboard.writeText(sessionId);
92
+ alert('Session ID copied to clipboard!');
93
+ };
94
+
95
+ return (
96
+ <div className="session-manager">
97
+ <div className="session-header">
98
+ <h2>Session Manager</h2>
99
+ <div className="session-info">
100
+ <span>Session ID: {sessionId}</span>
101
+ <button onClick={copySessionId}>Copy ID</button>
102
+ </div>
103
+ <button onClick={handleRefresh} disabled={loading}>
104
+ {loading ? 'Loading...' : 'Refresh'}
105
+ </button>
106
+ </div>
107
+
108
+ {error && (
109
+ <div className="error-message">
110
+ Error: {error}
111
+ </div>
112
+ )}
113
+
114
+ {quizDetected && (
115
+ <div className="quiz-alert">
116
+ 🎯 Quiz Detected! Click to launch Quiz App
117
+ <button onClick={() => launchQuizApp(sessionId)}>
118
+ Launch Quiz
119
+ </button>
120
+ </div>
121
+ )}
122
+
123
+ <div className="files-list">
124
+ <h3>Files ({files.length})</h3>
125
+ {files.length === 0 ? (
126
+ <p>No files yet. Use Claude to save files to this session.</p>
127
+ ) : (
128
+ <ul>
129
+ {files.map((file, index) => (
130
+ <FileItem key={index} file={file} sessionId={sessionId} />
131
+ ))}
132
+ </ul>
133
+ )}
134
+ </div>
135
+ </div>
136
+ );
137
+ }
138
+
139
+ // ===== FILE ITEM COMPONENT =====
140
+ function FileItem({ file, sessionId }) {
141
+ const [showContent, setShowContent] = useState(false);
142
+
143
+ const handleDownload = () => {
144
+ // Create a download link
145
+ const blob = new Blob([file.content || ''], { type: 'text/plain' });
146
+ const url = URL.createObjectURL(blob);
147
+ const a = document.createElement('a');
148
+ a.href = url;
149
+ a.download = file.name;
150
+ a.click();
151
+ URL.revokeObjectURL(url);
152
+ };
153
+
154
+ const getFileIcon = (fileName) => {
155
+ if (fileName.endsWith('.dart')) return '🎯';
156
+ if (fileName.endsWith('.tex')) return '📜';
157
+ if (fileName.endsWith('.json')) return '📋';
158
+ if (fileName.endsWith('.js') || fileName.endsWith('.ts')) return '📝';
159
+ if (fileName.endsWith('.py')) return '🐍';
160
+ return '📄';
161
+ };
162
+
163
+ return (
164
+ <li className="file-item">
165
+ <div className="file-header">
166
+ <span className="file-icon">{getFileIcon(file.name)}</span>
167
+ <span className="file-name">{file.name}</span>
168
+ <span className="file-size">({(file.size / 1024).toFixed(2)} KB)</span>
169
+ <button onClick={() => setShowContent(!showContent)}>
170
+ {showContent ? 'Hide' : 'Show'}
171
+ </button>
172
+ <button onClick={handleDownload}>Download</button>
173
+ </div>
174
+
175
+ {showContent && file.content && (
176
+ <div className="file-content">
177
+ <pre>{file.content.substring(0, 500)}</pre>
178
+ {file.content.length > 500 && <p>... (truncated)</p>}
179
+ </div>
180
+ )}
181
+ </li>
182
+ );
183
+ }
184
+
185
+ // ===== QUIZ APP LAUNCHER =====
186
+ function launchQuizApp(sessionId) {
187
+ // This function would navigate to your Quiz app with the session ID
188
+ window.location.href = `/apps/quiz?sessionId=${sessionId}`;
189
+ }
190
+
191
+ // ===== UTILITY FUNCTIONS =====
192
+
193
+ // Function to save a file from the frontend (for testing)
194
+ export async function saveFile(sessionId, fileName, content) {
195
+ try {
196
+ const response = await fetch(`${API_URL}/api/mcp-handler`, {
197
+ method: 'POST',
198
+ headers: {
199
+ 'Content-Type': 'application/json',
200
+ },
201
+ body: JSON.stringify({
202
+ sessionId,
203
+ action: 'save_file',
204
+ fileName,
205
+ content,
206
+ }),
207
+ });
208
+
209
+ const data = await response.json();
210
+ return data;
211
+ } catch (error) {
212
+ console.error('Error saving file:', error);
213
+ throw error;
214
+ }
215
+ }
216
+
217
+ // Function to clear all files for a session
218
+ export async function clearSession(sessionId) {
219
+ try {
220
+ const response = await fetch(`${API_URL}/api/mcp-handler`, {
221
+ method: 'POST',
222
+ headers: {
223
+ 'Content-Type': 'application/json',
224
+ },
225
+ body: JSON.stringify({
226
+ sessionId,
227
+ action: 'clear_session',
228
+ fileName: '',
229
+ content: '',
230
+ }),
231
+ });
232
+
233
+ const data = await response.json();
234
+ return data;
235
+ } catch (error) {
236
+ console.error('Error clearing session:', error);
237
+ throw error;
238
+ }
239
+ }
240
+
241
+ // ===== FLUTTER APP COMPONENT =====
242
+ export function FlutterAppViewer({ sessionId }) {
243
+ const [dartFiles, setDartFiles] = useState([]);
244
+
245
+ useEffect(() => {
246
+ const fetchDartFiles = async () => {
247
+ try {
248
+ const response = await fetch(`${API_URL}/api/mcp-handler?sessionId=${sessionId}`);
249
+ const data = await response.json();
250
+
251
+ if (data.success) {
252
+ // Filter only .dart files
253
+ const dartOnly = data.files.filter(f => f.name.endsWith('.dart'));
254
+ setDartFiles(dartOnly);
255
+ }
256
+ } catch (error) {
257
+ console.error('Error fetching Dart files:', error);
258
+ }
259
+ };
260
+
261
+ if (sessionId) {
262
+ fetchDartFiles();
263
+ const interval = setInterval(fetchDartFiles, POLL_INTERVAL);
264
+ return () => clearInterval(interval);
265
+ }
266
+ }, [sessionId]);
267
+
268
+ const openInZapp = (file) => {
269
+ // Open Dart code in Zapp runner
270
+ const zappUrl = 'https://zapp.run/';
271
+ const code = encodeURIComponent(file.content);
272
+ window.open(`${zappUrl}?code=${code}`, '_blank');
273
+ };
274
+
275
+ return (
276
+ <div className="flutter-viewer">
277
+ <h3>Flutter/Dart Files</h3>
278
+ {dartFiles.map((file, index) => (
279
+ <div key={index} className="dart-file">
280
+ <span>{file.name}</span>
281
+ <button onClick={() => openInZapp(file)}>
282
+ Open in Zapp
283
+ </button>
284
+ </div>
285
+ ))}
286
+ </div>
287
+ );
288
+ }
289
+
290
+ // ===== LATEX VIEWER COMPONENT =====
291
+ export function LaTeXViewer({ sessionId }) {
292
+ const [texFiles, setTexFiles] = useState([]);
293
+
294
+ useEffect(() => {
295
+ const fetchTexFiles = async () => {
296
+ try {
297
+ const response = await fetch(`${API_URL}/api/mcp-handler?sessionId=${sessionId}`);
298
+ const data = await response.json();
299
+
300
+ if (data.success) {
301
+ // Filter only .tex files
302
+ const texOnly = data.files.filter(f => f.name.endsWith('.tex'));
303
+ setTexFiles(texOnly);
304
+ }
305
+ } catch (error) {
306
+ console.error('Error fetching LaTeX files:', error);
307
+ }
308
+ };
309
+
310
+ if (sessionId) {
311
+ fetchTexFiles();
312
+ const interval = setInterval(fetchTexFiles, POLL_INTERVAL);
313
+ return () => clearInterval(interval);
314
+ }
315
+ }, [sessionId]);
316
+
317
+ return (
318
+ <div className="latex-viewer">
319
+ <h3>LaTeX Documents</h3>
320
+ {texFiles.map((file, index) => (
321
+ <div key={index} className="tex-file">
322
+ <span>{file.name}</span>
323
+ <button onClick={() => openInLatexEditor(file)}>
324
+ Open in Editor
325
+ </button>
326
+ </div>
327
+ ))}
328
+ </div>
329
+ );
330
+ }
331
+
332
+ function openInLatexEditor(file) {
333
+ // Your LaTeX editor integration
334
+ window.location.href = `/apps/latex-studio?file=${file.name}`;
335
+ }
336
+
337
+ // ===== EXAMPLE USAGE IN NEXT.JS PAGE =====
338
+ /*
339
+ // pages/index.js or pages/dashboard.js
340
+
341
+ import { SessionManager } from '../components/SessionManager';
342
+
343
+ export default function Dashboard() {
344
+ return (
345
+ <div className="dashboard">
346
+ <h1>Reuben OS - File Manager</h1>
347
+ <SessionManager />
348
+ </div>
349
+ );
350
+ }
351
+ */
352
+
353
+ // ===== CSS STYLES (add to your global styles or styled-components) =====
354
+ const styles = `
355
+ .session-manager {
356
+ max-width: 800px;
357
+ margin: 0 auto;
358
+ padding: 20px;
359
+ }
360
+
361
+ .session-header {
362
+ background: #f5f5f5;
363
+ padding: 15px;
364
+ border-radius: 8px;
365
+ margin-bottom: 20px;
366
+ }
367
+
368
+ .session-info {
369
+ display: flex;
370
+ align-items: center;
371
+ gap: 10px;
372
+ margin: 10px 0;
373
+ font-family: monospace;
374
+ background: white;
375
+ padding: 10px;
376
+ border-radius: 4px;
377
+ }
378
+
379
+ .error-message {
380
+ background: #fee;
381
+ color: #c00;
382
+ padding: 10px;
383
+ border-radius: 4px;
384
+ margin: 10px 0;
385
+ }
386
+
387
+ .quiz-alert {
388
+ background: #efe;
389
+ color: #060;
390
+ padding: 15px;
391
+ border-radius: 4px;
392
+ margin: 10px 0;
393
+ display: flex;
394
+ justify-content: space-between;
395
+ align-items: center;
396
+ }
397
+
398
+ .files-list {
399
+ background: white;
400
+ padding: 15px;
401
+ border-radius: 8px;
402
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
403
+ }
404
+
405
+ .file-item {
406
+ list-style: none;
407
+ padding: 10px;
408
+ border-bottom: 1px solid #eee;
409
+ }
410
+
411
+ .file-header {
412
+ display: flex;
413
+ align-items: center;
414
+ gap: 10px;
415
+ }
416
+
417
+ .file-icon {
418
+ font-size: 20px;
419
+ }
420
+
421
+ .file-name {
422
+ flex: 1;
423
+ font-weight: 500;
424
+ }
425
+
426
+ .file-size {
427
+ color: #666;
428
+ font-size: 0.9em;
429
+ }
430
+
431
+ .file-content {
432
+ margin-top: 10px;
433
+ padding: 10px;
434
+ background: #f5f5f5;
435
+ border-radius: 4px;
436
+ overflow-x: auto;
437
+ }
438
+
439
+ .file-content pre {
440
+ margin: 0;
441
+ font-size: 0.9em;
442
+ line-height: 1.4;
443
+ }
444
+
445
+ button {
446
+ padding: 6px 12px;
447
+ background: #007bff;
448
+ color: white;
449
+ border: none;
450
+ border-radius: 4px;
451
+ cursor: pointer;
452
+ }
453
+
454
+ button:hover {
455
+ background: #0056b3;
456
+ }
457
+
458
+ button:disabled {
459
+ opacity: 0.5;
460
+ cursor: not-allowed;
461
+ }
462
+ `;
mcp-server.js CHANGED
@@ -1,4 +1,5 @@
1
  #!/usr/bin/env node
 
2
 
3
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -7,17 +8,17 @@ import {
7
  ListToolsRequestSchema,
8
  } from '@modelcontextprotocol/sdk/types.js';
9
  import fetch from 'node-fetch';
10
- import { FormData, Blob } from 'node-fetch';
11
 
12
  const BASE_URL = process.env.REUBENOS_URL || 'http://localhost:3000';
 
13
 
14
  class ReubenOSMCPServer {
15
  constructor() {
16
  this.server = new Server(
17
  {
18
- name: 'reubenos-file-manager',
19
- version: '1.0.0',
20
- description: 'AI-powered desktop environment with file management and document generation',
21
  icon: '🖥️',
22
  },
23
  {
@@ -41,356 +42,103 @@ class ReubenOSMCPServer {
41
  this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
42
  tools: [
43
  {
44
- name: 'create_session',
45
- description: 'Create a new isolated session with a unique key',
46
- inputSchema: {
47
- type: 'object',
48
- properties: {
49
- metadata: {
50
- type: 'object',
51
- description: 'Optional metadata for the session',
52
- },
53
- },
54
- },
55
- },
56
- {
57
- name: 'verify_session',
58
- description: 'Verify if a session key is valid and active',
59
  inputSchema: {
60
  type: 'object',
61
  properties: {
62
  sessionId: {
63
  type: 'string',
64
- description: 'Session ID to verify (format: session_timestamp_hash)',
65
  },
66
- sessionKey: {
67
- type: 'string',
68
- description: '(Deprecated) Use sessionId instead',
69
- },
70
- },
71
- oneOf: [
72
- { required: ['sessionId'] },
73
- { required: ['sessionKey'] }
74
- ],
75
- },
76
- },
77
- {
78
- name: 'upload_file',
79
- description: 'Upload a file to session or public folder',
80
- inputSchema: {
81
- type: 'object',
82
- properties: {
83
- sessionKey: {
84
  type: 'string',
85
- description: 'Session ID for authentication (this will be auto-filled if you verified your session)',
 
86
  },
87
  fileName: {
88
  type: 'string',
89
- description: 'Name of the file',
90
  },
91
  content: {
92
  type: 'string',
93
- description: 'File content (base64 encoded for binary files)',
94
- },
95
- isPublic: {
96
- type: 'boolean',
97
- description: 'Save to public folder',
98
- default: false,
99
  },
100
  },
101
- required: ['fileName', 'content'],
102
  },
103
  },
104
  {
105
- name: 'download_file',
106
- description: 'Download a file from session or public folder',
107
  inputSchema: {
108
  type: 'object',
109
  properties: {
110
- sessionKey: {
111
- type: 'string',
112
- description: 'Session ID (not required for public files, auto-filled if you verified your session)',
113
- },
114
- fileName: {
115
- type: 'string',
116
- description: 'Name of the file to download',
117
- },
118
- isPublic: {
119
- type: 'boolean',
120
- description: 'Download from public folder',
121
- default: false,
122
- },
123
- },
124
- required: ['fileName'],
125
- },
126
- },
127
- {
128
- name: 'list_files',
129
- description: 'List files in session or public folder',
130
- inputSchema: {
131
- type: 'object',
132
- properties: {
133
- sessionKey: {
134
- type: 'string',
135
- description: 'Session ID (not required for public files, auto-filled if you verified your session)',
136
- },
137
- isPublic: {
138
- type: 'boolean',
139
- description: 'List public files',
140
- default: false,
141
- },
142
- },
143
- },
144
- },
145
- {
146
- name: 'generate_document',
147
- description: 'Generate DOCX, PDF, PowerPoint, or Excel documents',
148
- inputSchema: {
149
- type: 'object',
150
- properties: {
151
- sessionKey: {
152
- type: 'string',
153
- description: 'Session ID for authentication (this will be auto-filled if you verified your session)',
154
- },
155
- type: {
156
- type: 'string',
157
- enum: ['docx', 'pdf', 'ppt', 'excel', 'latex'],
158
- description: 'Document type to generate',
159
- },
160
- fileName: {
161
  type: 'string',
162
- description: 'Output file name',
163
  },
164
- content: {
165
  type: 'object',
166
- description: 'Document content (structure varies by type)',
167
- },
168
- isPublic: {
169
- type: 'boolean',
170
- description: 'Save to public folder',
171
- default: false,
172
- },
173
- },
174
- required: ['type', 'fileName', 'content'],
175
- },
176
- },
177
- {
178
- name: 'process_document',
179
- description: 'Read and analyze documents (DOCX, Excel, PDF, etc.)',
180
- inputSchema: {
181
- type: 'object',
182
- properties: {
183
- sessionKey: {
184
- type: 'string',
185
- description: 'Session ID for authentication (this will be auto-filled if you verified your session)',
186
- },
187
- fileName: {
188
- type: 'string',
189
- description: 'File name to process',
190
- },
191
- isPublic: {
192
- type: 'boolean',
193
- description: 'File is in public folder',
194
- default: false,
195
- },
196
- operation: {
197
- type: 'string',
198
- enum: ['read', 'analyze'],
199
- description: 'Operation to perform',
200
- default: 'read',
201
- },
202
- },
203
- required: ['fileName'],
204
- },
205
- },
206
- {
207
- name: 'create_flutter_app',
208
- description: 'Create a new Flutter app with Dart code that can be run in Zapp',
209
- inputSchema: {
210
- type: 'object',
211
- properties: {
212
- name: {
213
- type: 'string',
214
- description: 'Name of the Flutter app (alphanumeric, hyphens, underscores)',
215
- },
216
- dartCode: {
217
- type: 'string',
218
- description: 'Complete Dart/Flutter code (typically main.dart content)',
219
- },
220
- dependencies: {
221
- type: 'array',
222
- items: { type: 'string' },
223
- description: 'List of dependencies (e.g., ["http: ^0.13.0", "provider: ^6.0.0"])',
224
- },
225
- pubspecYaml: {
226
- type: 'string',
227
- description: 'Complete pubspec.yaml content (optional, will be auto-generated)',
228
- },
229
- },
230
- required: ['name', 'dartCode'],
231
- },
232
- },
233
- {
234
- name: 'get_flutter_app',
235
- description: 'Retrieve a Flutter app by name',
236
- inputSchema: {
237
- type: 'object',
238
- properties: {
239
- name: {
240
- type: 'string',
241
- description: 'Name of the Flutter app',
242
- },
243
- },
244
- required: ['name'],
245
- },
246
- },
247
- {
248
- name: 'list_flutter_apps',
249
- description: 'List all available Flutter apps',
250
- inputSchema: {
251
- type: 'object',
252
- properties: {},
253
- },
254
- },
255
- {
256
- name: 'update_flutter_app',
257
- description: 'Update an existing Flutter app',
258
- inputSchema: {
259
- type: 'object',
260
- properties: {
261
- name: {
262
- type: 'string',
263
- description: 'Name of the Flutter app to update',
264
- },
265
- dartCode: {
266
- type: 'string',
267
- description: 'New Dart/Flutter code (optional)',
268
- },
269
- dependencies: {
270
- type: 'array',
271
- items: { type: 'string' },
272
- description: 'New dependencies list (optional)',
273
- },
274
- pubspecYaml: {
275
- type: 'string',
276
- description: 'New pubspec.yaml content (optional)',
277
- },
278
- },
279
- required: ['name'],
280
- },
281
- },
282
- {
283
- name: 'delete_flutter_app',
284
- description: 'Delete a Flutter app',
285
- inputSchema: {
286
- type: 'object',
287
- properties: {
288
- name: {
289
- type: 'string',
290
- description: 'Name of the Flutter app to delete',
291
- },
292
- },
293
- required: ['name'],
294
- },
295
- },
296
- {
297
- name: 'create_latex_document',
298
- description: 'Create a LaTeX document that can be edited in LaTeX Studio',
299
- inputSchema: {
300
- type: 'object',
301
- properties: {
302
- sessionKey: {
303
- type: 'string',
304
- description: 'Session ID for authentication (auto-filled if session verified)',
305
- },
306
- fileName: {
307
- type: 'string',
308
- description: 'Name of the LaTeX file (without .tex extension)',
309
- },
310
- content: {
311
- type: 'string',
312
- description: 'LaTeX document content',
313
- },
314
- isPublic: {
315
- type: 'boolean',
316
- description: 'Save to public folder',
317
- default: false,
318
- },
319
- },
320
- required: ['fileName', 'content'],
321
- },
322
- },
323
- {
324
- name: 'get_latex_document',
325
- description: 'Retrieve a LaTeX document content',
326
- inputSchema: {
327
- type: 'object',
328
- properties: {
329
- sessionKey: {
330
- type: 'string',
331
- description: 'Session ID for authentication (auto-filled if session verified)',
332
- },
333
- fileName: {
334
- type: 'string',
335
- description: 'Name of the LaTeX file',
336
- },
337
- isPublic: {
338
- type: 'boolean',
339
- description: 'File is in public folder',
340
- default: false,
341
- },
342
- },
343
- required: ['fileName'],
344
- },
345
- },
346
- {
347
- name: 'update_latex_document',
348
- description: 'Update an existing LaTeX document',
349
- inputSchema: {
350
- type: 'object',
351
- properties: {
352
- sessionKey: {
353
- type: 'string',
354
- description: 'Session ID for authentication (auto-filled if session verified)',
355
- },
356
- fileName: {
357
- type: 'string',
358
- description: 'Name of the LaTeX file to update',
359
- },
360
- content: {
361
- type: 'string',
362
- description: 'New LaTeX content',
363
- },
364
- isPublic: {
365
- type: 'boolean',
366
- description: 'File is in public folder',
367
- default: false,
368
  },
369
  },
370
- required: ['fileName', 'content'],
371
- },
372
- },
373
- {
374
- name: 'compile_latex_to_pdf',
375
- description: 'Compile a LaTeX document to PDF',
376
- inputSchema: {
377
- type: 'object',
378
- properties: {
379
- sessionKey: {
380
- type: 'string',
381
- description: 'Session ID for authentication (auto-filled if session verified)',
382
- },
383
- fileName: {
384
- type: 'string',
385
- description: 'Name of the LaTeX file to compile',
386
- },
387
- isPublic: {
388
- type: 'boolean',
389
- description: 'File is in public folder',
390
- default: false,
391
- },
392
- },
393
- required: ['fileName'],
394
  },
395
  },
396
  ],
@@ -401,38 +149,10 @@ class ReubenOSMCPServer {
401
 
402
  try {
403
  switch (name) {
404
- case 'create_session':
405
- return await this.createSession(args);
406
- case 'verify_session':
407
- return await this.verifySession(args);
408
- case 'upload_file':
409
- return await this.uploadFile(args);
410
- case 'download_file':
411
- return await this.downloadFile(args);
412
- case 'list_files':
413
- return await this.listFiles(args);
414
- case 'generate_document':
415
- return await this.generateDocument(args);
416
- case 'process_document':
417
- return await this.processDocument(args);
418
- case 'create_flutter_app':
419
- return await this.createFlutterApp(args);
420
- case 'get_flutter_app':
421
- return await this.getFlutterApp(args);
422
- case 'list_flutter_apps':
423
- return await this.listFlutterApps(args);
424
- case 'update_flutter_app':
425
- return await this.updateFlutterApp(args);
426
- case 'delete_flutter_app':
427
- return await this.deleteFlutterApp(args);
428
- case 'create_latex_document':
429
- return await this.createLatexDocument(args);
430
- case 'get_latex_document':
431
- return await this.getLatexDocument(args);
432
- case 'update_latex_document':
433
- return await this.updateLatexDocument(args);
434
- case 'compile_latex_to_pdf':
435
- return await this.compileLatexToPdf(args);
436
  default:
437
  throw new Error(`Unknown tool: ${name}`);
438
  }
@@ -449,445 +169,264 @@ class ReubenOSMCPServer {
449
  });
450
  }
451
 
452
- async createSession(args) {
453
- const response = await fetch(`${BASE_URL}/api/sessions/create`, {
454
- method: 'POST',
455
- headers: { 'Content-Type': 'application/json' },
456
- body: JSON.stringify({ metadata: args.metadata || {} }),
457
- });
458
-
459
- const data = await response.json();
460
- return {
461
- content: [
462
- {
463
- type: 'text',
464
- text: data.success
465
- ? `Session created successfully!\nSession ID: ${data.session.id}\nSession Key: ${data.session.key}\n\nIMPORTANT: Save this session key securely. You'll need it for all file operations.`
466
- : `Failed to create session: ${data.error}`,
467
- },
468
- ],
469
- };
470
- }
471
-
472
- async verifySession(args) {
473
- // Accept either sessionId or sessionKey
474
- const sessionId = args.sessionId || args.sessionKey;
475
-
476
- const response = await fetch(`${BASE_URL}/api/sessions/verify`, {
477
- method: 'POST',
478
- headers: { 'Content-Type': 'application/json' },
479
- body: JSON.stringify({ sessionId }),
480
- });
481
-
482
- const data = await response.json();
483
-
484
- if (data.success && data.valid) {
485
- // Store the session ID for future use
486
- this.sessionId = sessionId;
487
-
488
- return {
489
- content: [
490
- {
491
- type: 'text',
492
- text: `✅ Session is VALID!\n\nSession ID: ${sessionId}\n\nYou can now use this session for file operations.`,
493
- },
494
- ],
495
- };
496
- } else {
497
- return {
498
- content: [
499
- {
500
- type: 'text',
501
- text: `❌ Session is INVALID or EXPIRED\n\nThe session ID you provided is not recognized by the server. This can happen if:\n- The Hugging Face Space restarted\n- The session expired (24 hours old)\n- The session ID was mistyped\n\nPlease get a new session ID from the ReubenOS Session Manager.`,
502
- },
503
- ],
504
- };
505
- }
506
- }
507
-
508
- async uploadFile(args) {
509
- const formData = new FormData();
510
- const buffer = Buffer.from(args.content, args.content.includes('base64,') ? 'base64' : 'utf8');
511
- formData.append('file', new Blob([buffer]), args.fileName);
512
- formData.append('public', args.isPublic ? 'true' : 'false');
513
-
514
- // Use public endpoint if public, sessions endpoint if private
515
- const endpoint = args.isPublic
516
- ? `${BASE_URL}/api/public/upload`
517
- : `${BASE_URL}/api/sessions/upload`;
518
-
519
- const headers = args.isPublic
520
- ? {}
521
- : { 'x-session-id': args.sessionKey || this.sessionId };
522
-
523
- const response = await fetch(endpoint, {
524
- method: 'POST',
525
- headers,
526
- body: formData,
527
- });
528
-
529
- const data = await response.json();
530
- return {
531
- content: [
532
- {
533
- type: 'text',
534
- text: data.success
535
- ? `File uploaded successfully!\nFile: ${data.fileName}\nSize: ${data.size} bytes\nLocation: ${data.isPublic ? 'Public (data/public/)' : 'Private Session (data/files/)'}\n${args.isPublic ? 'Note: This file is accessible to everyone!' : 'Note: This file is private and only accessible with your session key.'}`
536
- : `Failed to upload file: ${data.error}`,
537
- },
538
- ],
539
- };
540
- }
541
-
542
- async downloadFile(args) {
543
- const url = `${BASE_URL}/api/sessions/download?file=${encodeURIComponent(args.fileName)}${
544
- args.isPublic ? '&public=true' : ''
545
- }`;
546
- const headers = args.isPublic ? {} : { 'x-session-key': args.sessionKey };
547
-
548
- const response = await fetch(url, { headers });
549
-
550
- if (response.ok) {
551
- const buffer = await response.buffer();
552
- const base64 = buffer.toString('base64');
553
- return {
554
- content: [
555
- {
556
- type: 'text',
557
- text: `File downloaded successfully!\nFile: ${args.fileName}\nSize: ${buffer.length} bytes\nContent (base64): ${base64.substring(0, 100)}...`,
558
- },
559
- ],
560
- };
561
- } else {
562
- return {
563
- content: [
564
- {
565
- type: 'text',
566
- text: `Failed to download file: ${response.statusText}`,
567
- },
568
- ],
569
- };
570
- }
571
- }
572
-
573
- async listFiles(args) {
574
- const url = `${BASE_URL}/api/sessions/files${args.isPublic ? '?public=true' : ''}`;
575
- const headers = args.isPublic ? {} : { 'x-session-id': args.sessionKey || this.sessionId };
576
-
577
- const response = await fetch(url, { headers });
578
- const data = await response.json();
579
-
580
- if (data.success) {
581
- const fileList = data.files
582
- .map((f) => `- ${f.name} (${(f.size / 1024).toFixed(2)} KB)`)
583
- .join('\n');
584
- return {
585
- content: [
586
- {
587
- type: 'text',
588
- text: `Files in ${data.type} folder (${data.count} files):\n${fileList || 'No files found'}`,
589
- },
590
- ],
591
- };
592
- } else {
593
- return {
594
- content: [
595
- {
596
- type: 'text',
597
- text: `Failed to list files: ${data.error}`,
598
- },
599
- ],
600
- };
601
- }
602
- }
603
-
604
- async generateDocument(args) {
605
- const response = await fetch(`${BASE_URL}/api/documents/generate`, {
606
- method: 'POST',
607
- headers: {
608
- 'Content-Type': 'application/json',
609
- 'x-session-id': args.sessionKey || this.sessionId,
610
- },
611
- body: JSON.stringify({
612
- type: args.type,
613
- fileName: args.fileName,
614
- content: args.content,
615
- isPublic: args.isPublic || false,
616
- }),
617
- });
618
-
619
- const data = await response.json();
620
- return {
621
- content: [
622
- {
623
- type: 'text',
624
- text: data.success
625
- ? `Document generated successfully!\nType: ${data.type.toUpperCase()}\nFile: ${data.fileName}\nSize: ${data.size} bytes\nLocation: ${data.isPublic ? 'Public' : 'Session'} folder`
626
- : `Failed to generate document: ${data.error}`,
627
- },
628
- ],
629
- };
630
- }
631
 
632
- async processDocument(args) {
633
- const response = await fetch(`${BASE_URL}/api/documents/process`, {
634
- method: 'POST',
635
- headers: {
636
- 'Content-Type': 'application/json',
637
- 'x-session-id': args.sessionKey || this.sessionId,
638
- },
639
- body: JSON.stringify({
640
- fileName: args.fileName,
641
- isPublic: args.isPublic || false,
642
- operation: args.operation || 'read',
643
- }),
644
- });
645
 
646
- const data = await response.json();
647
- return {
648
- content: [
649
- {
650
- type: 'text',
651
- text: data.success
652
- ? `Document processed:\nFile: ${data.fileName}\nType: ${data.content.type}\nContent: ${JSON.stringify(data.content, null, 2)}`
653
- : `Failed to process document: ${data.error}`,
654
- },
655
- ],
656
- };
657
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
 
659
- async createFlutterApp(args) {
660
- const response = await fetch(`${BASE_URL}/api/flutter/create`, {
661
- method: 'POST',
662
- headers: { 'Content-Type': 'application/json' },
663
- body: JSON.stringify({
664
- name: args.name,
665
- dartCode: args.dartCode,
666
- dependencies: args.dependencies || [],
667
- pubspecYaml: args.pubspecYaml,
668
- }),
669
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
670
 
671
- const data = await response.json();
672
- return {
673
- content: [
674
- {
675
- type: 'text',
676
- text: data.success
677
- ? `✅ Flutter app "${data.appName}" created successfully!\n\n📱 App Details:\n- Name: ${data.appName}\n- Dependencies: ${data.dependencies?.length || 0}\n- File: ${data.fileName}\n\n🎯 Next Steps:\n1. Open ReubenOS File Manager\n2. Navigate to flutter_apps folder\n3. Double-click "${data.appName}" to open in Zapp runner\n4. Copy the code and paste into Zapp's lib/main.dart\n5. Add dependencies to pubspec.yaml\n6. Click Run in Zapp!\n\n🚀 Your Flutter app is ready to test!`
678
- : `❌ Failed to create Flutter app: ${data.error}`,
679
- },
680
- ],
681
- };
682
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
683
 
684
- async getFlutterApp(args) {
685
- const response = await fetch(`${BASE_URL}/api/flutter/get?name=${encodeURIComponent(args.name)}`);
686
- const data = await response.json();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
687
 
688
- if (data.success) {
689
- return {
690
- content: [
691
- {
692
- type: 'text',
693
- text: `Flutter App: ${data.appData.name}\n\nDart Code:\n${data.appData.dartCode}\n\nDependencies:\n${data.appData.dependencies?.join('\n') || 'None'}\n\nCreated: ${data.appData.metadata?.created || 'Unknown'}`,
694
- },
695
- ],
696
- };
697
- } else {
 
 
698
  return {
699
  content: [
700
  {
701
  type: 'text',
702
- text: `Failed to get Flutter app: ${data.error}`,
703
  },
704
  ],
705
  };
706
  }
707
  }
708
 
709
- async listFlutterApps(args) {
710
- const response = await fetch(`${BASE_URL}/api/flutter/list`);
711
- const data = await response.json();
 
 
 
 
 
 
 
 
 
 
712
 
713
- if (data.success) {
714
- const appList = data.apps
715
- .map((app) => `📱 ${app.name} (${app.dependenciesCount} deps) - Created: ${new Date(app.created).toLocaleDateString()}`)
716
- .join('\n');
717
- return {
718
- content: [
719
- {
720
- type: 'text',
721
- text: `Flutter Apps (${data.count} total):\n\n${appList || 'No Flutter apps found'}`,
722
- },
723
- ],
724
- };
725
- } else {
726
- return {
727
- content: [
728
- {
729
- type: 'text',
730
- text: `Failed to list Flutter apps: ${data.error}`,
731
- },
732
- ],
733
  };
734
- }
735
- }
736
-
737
- async updateFlutterApp(args) {
738
- const response = await fetch(`${BASE_URL}/api/flutter/update`, {
739
- method: 'PUT',
740
- headers: { 'Content-Type': 'application/json' },
741
- body: JSON.stringify({
742
- name: args.name,
743
- dartCode: args.dartCode,
744
- dependencies: args.dependencies,
745
- pubspecYaml: args.pubspecYaml,
746
- }),
747
- });
748
-
749
- const data = await response.json();
750
- return {
751
- content: [
752
- {
753
- type: 'text',
754
- text: data.success
755
- ? `✅ Flutter app "${data.appName}" updated successfully!`
756
- : `❌ Failed to update Flutter app: ${data.error}`,
757
- },
758
- ],
759
- };
760
- }
761
-
762
- async deleteFlutterApp(args) {
763
- const response = await fetch(`${BASE_URL}/api/flutter/delete?name=${encodeURIComponent(args.name)}`, {
764
- method: 'DELETE',
765
- });
766
-
767
- const data = await response.json();
768
- return {
769
- content: [
770
- {
771
- type: 'text',
772
- text: data.success
773
- ? `✅ Flutter app "${args.name}" deleted successfully!`
774
- : `❌ Failed to delete Flutter app: ${data.error}`,
775
- },
776
- ],
777
- };
778
- }
779
-
780
- async createLatexDocument(args) {
781
- const response = await fetch(`${BASE_URL}/api/latex/save`, {
782
- method: 'POST',
783
- headers: {
784
- 'Content-Type': 'application/json',
785
- 'x-session-id': args.sessionKey || this.sessionId,
786
- },
787
- body: JSON.stringify({
788
- fileName: args.fileName.endsWith('.tex') ? args.fileName : `${args.fileName}.tex`,
789
- content: args.content,
790
- isPublic: args.isPublic || false,
791
- }),
792
- });
793
 
794
- const data = await response.json();
795
- return {
796
- content: [
797
- {
798
- type: 'text',
799
- text: data.success
800
- ? `✅ LaTeX document created successfully!\n\n📄 Document: ${data.fileName}\n📂 Location: ${args.isPublic ? 'Public' : 'Session'} folder\n\n🎯 Next Steps:\n1. Open ReubenOS File Manager\n2. Navigate to ${args.isPublic ? 'Public' : 'My Files'}\n3. Double-click "${data.fileName}" to open in LaTeX Studio\n4. Edit and compile your document\n\n✨ Your LaTeX document is ready!`
801
- : `❌ Failed to create LaTeX document: ${data.error}`,
802
- },
803
- ],
804
- };
805
- }
806
 
807
- async getLatexDocument(args) {
808
- // First download the file to get its content
809
- const url = `${BASE_URL}/api/sessions/download?file=${encodeURIComponent(args.fileName)}${
810
- args.isPublic ? '&public=true' : ''
811
- }`;
812
- const headers = args.isPublic ? {} : { 'x-session-id': args.sessionKey || this.sessionId };
813
 
814
- const response = await fetch(url, { headers });
 
815
 
816
- if (response.ok) {
817
- const content = await response.text();
818
- return {
819
- content: [
820
- {
821
- type: 'text',
822
- text: `LaTeX Document: ${args.fileName}\n\n${content}`,
823
- },
824
- ],
825
- };
826
- } else {
 
 
 
 
 
 
 
 
 
827
  return {
828
  content: [
829
  {
830
  type: 'text',
831
- text: `Failed to get LaTeX document: ${response.statusText}`,
832
  },
833
  ],
834
  };
835
  }
836
  }
837
 
838
- async updateLatexDocument(args) {
839
- const response = await fetch(`${BASE_URL}/api/latex/save`, {
840
- method: 'POST',
841
- headers: {
842
- 'Content-Type': 'application/json',
843
- 'x-session-id': args.sessionKey || this.sessionId,
844
- },
845
- body: JSON.stringify({
846
- fileName: args.fileName.endsWith('.tex') ? args.fileName : `${args.fileName}.tex`,
847
- content: args.content,
848
- isPublic: args.isPublic || false,
849
- }),
850
- });
851
-
852
- const data = await response.json();
853
- return {
854
- content: [
855
- {
856
- type: 'text',
857
- text: data.success
858
- ? `✅ LaTeX document "${args.fileName}" updated successfully!`
859
- : `❌ Failed to update LaTeX document: ${data.error}`,
860
- },
861
- ],
862
- };
863
- }
864
-
865
- async compileLatexToPdf(args) {
866
- const response = await fetch(`${BASE_URL}/api/latex/compile`, {
867
- method: 'POST',
868
- headers: {
869
- 'Content-Type': 'application/json',
870
- 'x-session-id': args.sessionKey || this.sessionId,
871
- },
872
- body: JSON.stringify({
873
- fileName: args.fileName.endsWith('.tex') ? args.fileName : `${args.fileName}.tex`,
874
- isPublic: args.isPublic || false,
875
- }),
876
- });
877
-
878
- const data = await response.json();
879
- return {
880
- content: [
881
- {
882
- type: 'text',
883
- text: data.success
884
- ? `✅ LaTeX compiled to PDF successfully!\n\n📄 PDF File: ${data.pdfFileName}\n📂 Location: ${args.isPublic ? 'Public' : 'Session'} folder\n\n✨ Your PDF is ready for download!`
885
- : `❌ Failed to compile LaTeX: ${data.error}`,
886
- },
887
- ],
888
- };
889
- }
890
-
891
  async run() {
892
  const transport = new StdioServerTransport();
893
  await this.server.connect(transport);
 
1
  #!/usr/bin/env node
2
+ // mcp-server.js - Simplified MCP Server for Reuben OS with Stateless Prefix Strategy
3
 
4
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
 
8
  ListToolsRequestSchema,
9
  } from '@modelcontextprotocol/sdk/types.js';
10
  import fetch from 'node-fetch';
 
11
 
12
  const BASE_URL = process.env.REUBENOS_URL || 'http://localhost:3000';
13
+ const API_ENDPOINT = `${BASE_URL}/api/mcp-handler`;
14
 
15
  class ReubenOSMCPServer {
16
  constructor() {
17
  this.server = new Server(
18
  {
19
+ name: 'reubenos-mcp-server',
20
+ version: '2.0.0',
21
+ description: 'Simplified MCP Server for Reuben OS with stateless prefix strategy',
22
  icon: '🖥️',
23
  },
24
  {
 
42
  this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
43
  tools: [
44
  {
45
+ name: 'manage_files',
46
+ description: 'Manage files in Reuben OS - save Flutter/Dart, LaTeX, text, or any code files. Requires sessionId.',
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  inputSchema: {
48
  type: 'object',
49
  properties: {
50
  sessionId: {
51
  type: 'string',
52
+ description: 'Session ID from Reuben OS (required) - get this from the UI',
53
  },
54
+ action: {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  type: 'string',
56
+ enum: ['save', 'retrieve', 'delete', 'clear'],
57
+ description: 'Action to perform',
58
  },
59
  fileName: {
60
  type: 'string',
61
+ description: 'File name (required for save/delete). Examples: main.dart, document.tex, script.js',
62
  },
63
  content: {
64
  type: 'string',
65
+ description: 'File content (required for save action)',
 
 
 
 
 
66
  },
67
  },
68
+ required: ['sessionId', 'action'],
69
  },
70
  },
71
  {
72
+ name: 'deploy_quiz',
73
+ description: 'Deploy an interactive quiz to Reuben OS. The quiz will be shown in the Quiz App.',
74
  inputSchema: {
75
  type: 'object',
76
  properties: {
77
+ sessionId: {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  type: 'string',
79
+ description: 'Session ID from Reuben OS (required) - get this from the UI',
80
  },
81
+ quizData: {
82
  type: 'object',
83
+ description: 'Quiz data object',
84
+ properties: {
85
+ title: {
86
+ type: 'string',
87
+ description: 'Quiz title',
88
+ },
89
+ description: {
90
+ type: 'string',
91
+ description: 'Quiz description',
92
+ },
93
+ timeLimit: {
94
+ type: 'number',
95
+ description: 'Time limit in minutes (optional)',
96
+ },
97
+ questions: {
98
+ type: 'array',
99
+ description: 'Array of quiz questions',
100
+ items: {
101
+ type: 'object',
102
+ properties: {
103
+ id: {
104
+ type: 'string',
105
+ description: 'Unique question ID',
106
+ },
107
+ question: {
108
+ type: 'string',
109
+ description: 'The question text',
110
+ },
111
+ type: {
112
+ type: 'string',
113
+ enum: ['multiple-choice', 'true-false', 'short-answer'],
114
+ description: 'Question type',
115
+ },
116
+ options: {
117
+ type: 'array',
118
+ description: 'Answer options (for multiple-choice)',
119
+ items: { type: 'string' },
120
+ },
121
+ correctAnswer: {
122
+ type: ['string', 'number', 'boolean'],
123
+ description: 'Correct answer or index',
124
+ },
125
+ explanation: {
126
+ type: 'string',
127
+ description: 'Explanation (optional)',
128
+ },
129
+ points: {
130
+ type: 'number',
131
+ description: 'Points (default: 1)',
132
+ },
133
+ },
134
+ required: ['id', 'question', 'type', 'correctAnswer'],
135
+ },
136
+ },
137
+ },
138
+ required: ['title', 'questions'],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  },
140
  },
141
+ required: ['sessionId', 'quizData'],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  },
143
  },
144
  ],
 
149
 
150
  try {
151
  switch (name) {
152
+ case 'manage_files':
153
+ return await this.manageFiles(args);
154
+ case 'deploy_quiz':
155
+ return await this.deployQuiz(args);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  default:
157
  throw new Error(`Unknown tool: ${name}`);
158
  }
 
169
  });
170
  }
171
 
172
+ async manageFiles(args) {
173
+ try {
174
+ // Validate inputs based on action
175
+ if (args.action === 'save' && (!args.fileName || args.content === undefined)) {
176
+ return {
177
+ content: [
178
+ {
179
+ type: 'text',
180
+ text: '❌ Save action requires fileName and content',
181
+ },
182
+ ],
183
+ };
184
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
+ if (args.action === 'delete' && !args.fileName) {
187
+ return {
188
+ content: [
189
+ {
190
+ type: 'text',
191
+ text: '❌ Delete action requires fileName',
192
+ },
193
+ ],
194
+ };
195
+ }
 
 
 
196
 
197
+ // Handle different actions
198
+ switch (args.action) {
199
+ case 'save': {
200
+ const response = await fetch(API_ENDPOINT, {
201
+ method: 'POST',
202
+ headers: { 'Content-Type': 'application/json' },
203
+ body: JSON.stringify({
204
+ sessionId: args.sessionId,
205
+ action: 'save_file',
206
+ fileName: args.fileName,
207
+ content: args.content,
208
+ }),
209
+ });
210
+
211
+ const data = await response.json();
212
+
213
+ if (response.ok && data.success) {
214
+ return {
215
+ content: [
216
+ {
217
+ type: 'text',
218
+ text: `✅ File saved successfully!\n\n📄 File: ${args.fileName}\n🔑 Session: ${args.sessionId}\n\n✨ File is now available in Reuben OS`,
219
+ },
220
+ ],
221
+ };
222
+ } else {
223
+ return {
224
+ content: [
225
+ {
226
+ type: 'text',
227
+ text: `❌ Failed to save file: ${data.error || 'Unknown error'}`,
228
+ },
229
+ ],
230
+ };
231
+ }
232
+ }
233
 
234
+ case 'retrieve': {
235
+ const response = await fetch(`${API_ENDPOINT}?sessionId=${args.sessionId}`, {
236
+ method: 'GET',
237
+ headers: { 'Content-Type': 'application/json' },
238
+ });
239
+
240
+ const data = await response.json();
241
+
242
+ if (response.ok && data.success) {
243
+ const fileList = data.files
244
+ .map(f => `📄 ${f.name} (${(f.size / 1024).toFixed(2)} KB)${f.isQuiz ? ' [QUIZ]' : ''}`)
245
+ .join('\n');
246
+
247
+ return {
248
+ content: [
249
+ {
250
+ type: 'text',
251
+ text: `📁 Files for session ${args.sessionId}:\n\n${fileList || 'No files found'}\n\nTotal: ${data.count} file(s)`,
252
+ },
253
+ ],
254
+ };
255
+ } else {
256
+ return {
257
+ content: [
258
+ {
259
+ type: 'text',
260
+ text: `❌ Failed to retrieve files: ${data.error || 'Unknown error'}`,
261
+ },
262
+ ],
263
+ };
264
+ }
265
+ }
266
 
267
+ case 'delete': {
268
+ const response = await fetch(API_ENDPOINT, {
269
+ method: 'POST',
270
+ headers: { 'Content-Type': 'application/json' },
271
+ body: JSON.stringify({
272
+ sessionId: args.sessionId,
273
+ action: 'delete_file',
274
+ fileName: args.fileName,
275
+ content: '',
276
+ }),
277
+ });
278
+
279
+ const data = await response.json();
280
+
281
+ if (response.ok && data.success) {
282
+ return {
283
+ content: [
284
+ {
285
+ type: 'text',
286
+ text: `✅ File '${args.fileName}' deleted successfully from session ${args.sessionId}`,
287
+ },
288
+ ],
289
+ };
290
+ } else {
291
+ return {
292
+ content: [
293
+ {
294
+ type: 'text',
295
+ text: `❌ Failed to delete file: ${data.error || 'File not found'}`,
296
+ },
297
+ ],
298
+ };
299
+ }
300
+ }
301
 
302
+ case 'clear': {
303
+ const response = await fetch(API_ENDPOINT, {
304
+ method: 'POST',
305
+ headers: { 'Content-Type': 'application/json' },
306
+ body: JSON.stringify({
307
+ sessionId: args.sessionId,
308
+ action: 'clear_session',
309
+ fileName: '',
310
+ content: '',
311
+ }),
312
+ });
313
+
314
+ const data = await response.json();
315
+
316
+ if (response.ok && data.success) {
317
+ return {
318
+ content: [
319
+ {
320
+ type: 'text',
321
+ text: `✅ All files cleared for session ${args.sessionId}\n\n🗑️ Deleted ${data.deletedCount || 0} file(s)`,
322
+ },
323
+ ],
324
+ };
325
+ } else {
326
+ return {
327
+ content: [
328
+ {
329
+ type: 'text',
330
+ text: `❌ Failed to clear session: ${data.error || 'Unknown error'}`,
331
+ },
332
+ ],
333
+ };
334
+ }
335
+ }
336
 
337
+ default:
338
+ return {
339
+ content: [
340
+ {
341
+ type: 'text',
342
+ text: `❌ Unknown action: ${args.action}`,
343
+ },
344
+ ],
345
+ };
346
+ }
347
+ } catch (error) {
348
+ console.error('manage_files error:', error);
349
  return {
350
  content: [
351
  {
352
  type: 'text',
353
+ text: `❌ Error: ${error.message}`,
354
  },
355
  ],
356
  };
357
  }
358
  }
359
 
360
+ async deployQuiz(args) {
361
+ try {
362
+ // Validate quiz data
363
+ if (!args.quizData.questions || args.quizData.questions.length === 0) {
364
+ return {
365
+ content: [
366
+ {
367
+ type: 'text',
368
+ text: '❌ Quiz must have at least one question',
369
+ },
370
+ ],
371
+ };
372
+ }
373
 
374
+ // Add metadata to quiz
375
+ const fullQuizData = {
376
+ ...args.quizData,
377
+ createdAt: new Date().toISOString(),
378
+ sessionId: args.sessionId,
379
+ version: '1.0',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
 
382
+ // Save quiz as quiz.json
383
+ const response = await fetch(API_ENDPOINT, {
384
+ method: 'POST',
385
+ headers: { 'Content-Type': 'application/json' },
386
+ body: JSON.stringify({
387
+ sessionId: args.sessionId,
388
+ action: 'deploy_quiz',
389
+ fileName: 'quiz.json',
390
+ content: JSON.stringify(fullQuizData, null, 2),
391
+ }),
392
+ });
 
393
 
394
+ const data = await response.json();
 
 
 
 
 
395
 
396
+ if (response.ok && data.success) {
397
+ const totalPoints = args.quizData.questions.reduce((sum, q) => sum + (q.points || 1), 0);
398
 
399
+ return {
400
+ content: [
401
+ {
402
+ type: 'text',
403
+ text: `✅ Quiz deployed successfully!\n\n📝 Quiz: ${args.quizData.title}\n📊 Questions: ${args.quizData.questions.length}\n⏱️ Time Limit: ${args.quizData.timeLimit || 'No limit'} minutes\n🎯 Total Points: ${totalPoints}\n🔑 Session: ${args.sessionId}\n\n🚀 The quiz is now available in Reuben OS Quiz App!`,
404
+ },
405
+ ],
406
+ };
407
+ } else {
408
+ return {
409
+ content: [
410
+ {
411
+ type: 'text',
412
+ text: `❌ Failed to deploy quiz: ${data.error || 'Unknown error'}`,
413
+ },
414
+ ],
415
+ };
416
+ }
417
+ } catch (error) {
418
+ console.error('deploy_quiz error:', error);
419
  return {
420
  content: [
421
  {
422
  type: 'text',
423
+ text: `❌ Error deploying quiz: ${error.message}`,
424
  },
425
  ],
426
  };
427
  }
428
  }
429
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  async run() {
431
  const transport = new StdioServerTransport();
432
  await this.server.connect(transport);
pages/api/mcp-handler.js ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // pages/api/mcp-handler.js
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+
5
+ // Ensure /tmp directory exists
6
+ async function ensureTmpDirectory() {
7
+ const tmpPath = '/tmp';
8
+ try {
9
+ await fs.access(tmpPath);
10
+ } catch {
11
+ await fs.mkdir(tmpPath, { recursive: true });
12
+ }
13
+ }
14
+
15
+ // Validate session ID format (alphanumeric and underscores only)
16
+ function isValidSessionId(sessionId) {
17
+ return sessionId && /^[a-zA-Z0-9_-]+$/.test(sessionId);
18
+ }
19
+
20
+ // Sanitize filename to prevent path traversal
21
+ function sanitizeFileName(fileName) {
22
+ return fileName.replace(/[^a-zA-Z0-9._-]/g, '_');
23
+ }
24
+
25
+ export default async function handler(req, res) {
26
+ // Enable CORS for all origins (adjust as needed for production)
27
+ res.setHeader('Access-Control-Allow-Origin', '*');
28
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
29
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
30
+
31
+ if (req.method === 'OPTIONS') {
32
+ return res.status(200).end();
33
+ }
34
+
35
+ try {
36
+ await ensureTmpDirectory();
37
+
38
+ if (req.method === 'POST') {
39
+ // Handle file save/quiz deployment
40
+ const { sessionId, action, fileName, content } = req.body;
41
+
42
+ // Validate session ID
43
+ if (!isValidSessionId(sessionId)) {
44
+ return res.status(400).json({
45
+ success: false,
46
+ error: 'Invalid or missing sessionId'
47
+ });
48
+ }
49
+
50
+ // Validate required fields
51
+ if (!action || !fileName || content === undefined) {
52
+ return res.status(400).json({
53
+ success: false,
54
+ error: 'Missing required fields: action, fileName, or content'
55
+ });
56
+ }
57
+
58
+ const sanitizedFileName = sanitizeFileName(fileName);
59
+ const prefixedFileName = `${sessionId}_${sanitizedFileName}`;
60
+ const filePath = path.join('/tmp', prefixedFileName);
61
+
62
+ try {
63
+ // Handle different actions
64
+ switch (action) {
65
+ case 'save_file':
66
+ case 'deploy_quiz':
67
+ // Write content to file
68
+ await fs.writeFile(filePath, content, 'utf8');
69
+
70
+ return res.status(200).json({
71
+ success: true,
72
+ message: `File saved successfully`,
73
+ fileName: sanitizedFileName,
74
+ prefixedFileName: prefixedFileName,
75
+ action: action
76
+ });
77
+
78
+ case 'delete_file':
79
+ // Delete specific file
80
+ try {
81
+ await fs.unlink(filePath);
82
+ return res.status(200).json({
83
+ success: true,
84
+ message: `File deleted successfully`,
85
+ fileName: sanitizedFileName
86
+ });
87
+ } catch (err) {
88
+ return res.status(404).json({
89
+ success: false,
90
+ error: 'File not found'
91
+ });
92
+ }
93
+
94
+ case 'clear_session':
95
+ // Clear all files for this session
96
+ const files = await fs.readdir('/tmp');
97
+ const sessionFiles = files.filter(f => f.startsWith(`${sessionId}_`));
98
+
99
+ for (const file of sessionFiles) {
100
+ await fs.unlink(path.join('/tmp', file));
101
+ }
102
+
103
+ return res.status(200).json({
104
+ success: true,
105
+ message: `Cleared ${sessionFiles.length} files for session`,
106
+ deletedCount: sessionFiles.length
107
+ });
108
+
109
+ default:
110
+ return res.status(400).json({
111
+ success: false,
112
+ error: `Unknown action: ${action}`
113
+ });
114
+ }
115
+ } catch (error) {
116
+ console.error('File operation error:', error);
117
+ return res.status(500).json({
118
+ success: false,
119
+ error: `Failed to ${action}: ${error.message}`
120
+ });
121
+ }
122
+
123
+ } else if (req.method === 'GET') {
124
+ // Handle file retrieval
125
+ const { sessionId } = req.query;
126
+
127
+ // Validate session ID
128
+ if (!isValidSessionId(sessionId)) {
129
+ return res.status(400).json({
130
+ success: false,
131
+ error: 'Invalid or missing sessionId'
132
+ });
133
+ }
134
+
135
+ try {
136
+ // Read all files from /tmp
137
+ const allFiles = await fs.readdir('/tmp');
138
+
139
+ // Filter files for this session
140
+ const sessionPrefix = `${sessionId}_`;
141
+ const sessionFiles = allFiles.filter(f => f.startsWith(sessionPrefix));
142
+
143
+ // Build file list with content
144
+ const fileList = await Promise.all(
145
+ sessionFiles.map(async (file) => {
146
+ const filePath = path.join('/tmp', file);
147
+ const stats = await fs.stat(filePath);
148
+
149
+ // Strip session prefix from filename
150
+ const cleanFileName = file.substring(sessionPrefix.length);
151
+
152
+ // Read file content (with size limit to prevent memory issues)
153
+ let content = null;
154
+ if (stats.size < 1024 * 1024) { // 1MB limit for content retrieval
155
+ try {
156
+ content = await fs.readFile(filePath, 'utf8');
157
+ } catch (err) {
158
+ console.error(`Error reading file ${file}:`, err);
159
+ }
160
+ }
161
+
162
+ return {
163
+ name: cleanFileName,
164
+ prefixedName: file,
165
+ size: stats.size,
166
+ modified: stats.mtime,
167
+ isQuiz: cleanFileName === 'quiz.json',
168
+ content: content
169
+ };
170
+ })
171
+ );
172
+
173
+ // Sort by modification time (newest first)
174
+ fileList.sort((a, b) => new Date(b.modified) - new Date(a.modified));
175
+
176
+ return res.status(200).json({
177
+ success: true,
178
+ sessionId: sessionId,
179
+ files: fileList,
180
+ count: fileList.length
181
+ });
182
+
183
+ } catch (error) {
184
+ console.error('Error reading files:', error);
185
+ return res.status(500).json({
186
+ success: false,
187
+ error: `Failed to retrieve files: ${error.message}`
188
+ });
189
+ }
190
+
191
+ } else {
192
+ return res.status(405).json({
193
+ success: false,
194
+ error: 'Method not allowed'
195
+ });
196
+ }
197
+
198
+ } catch (error) {
199
+ console.error('Unexpected error:', error);
200
+ return res.status(500).json({
201
+ success: false,
202
+ error: `Server error: ${error.message}`
203
+ });
204
+ }
205
+ }