mrmd-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,371 @@
1
+ /**
2
+ * http-shim.js - Drop-in replacement for Electron's electronAPI
3
+ *
4
+ * This shim allows the Electron UI (index.html) to work in a browser
5
+ * by replacing IPC calls with HTTP/WebSocket calls to mrmd-server.
6
+ *
7
+ * Usage:
8
+ * <script src="/http-shim.js"></script>
9
+ * <!-- Now window.electronAPI is available -->
10
+ */
11
+
12
+ (function() {
13
+ 'use strict';
14
+
15
+ // Get configuration from URL or defaults
16
+ const params = new URLSearchParams(window.location.search);
17
+ const TOKEN = params.get('token') || '';
18
+ const BASE_URL = window.MRMD_SERVER_URL || window.location.origin;
19
+
20
+ // ==========================================================================
21
+ // HTTP Client
22
+ // ==========================================================================
23
+
24
+ async function apiCall(method, path, body = null) {
25
+ const url = new URL(path, BASE_URL);
26
+ if (TOKEN) {
27
+ url.searchParams.set('token', TOKEN);
28
+ }
29
+
30
+ const options = {
31
+ method,
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ },
35
+ };
36
+
37
+ if (body !== null) {
38
+ options.body = JSON.stringify(body);
39
+ }
40
+
41
+ const response = await fetch(url.toString(), options);
42
+
43
+ if (!response.ok) {
44
+ const error = await response.json().catch(() => ({ error: response.statusText }));
45
+ throw new Error(error.error || error.message || `HTTP ${response.status}`);
46
+ }
47
+
48
+ return response.json();
49
+ }
50
+
51
+ const GET = (path) => apiCall('GET', path);
52
+ const POST = (path, body) => apiCall('POST', path, body);
53
+ const DELETE = (path) => apiCall('DELETE', path);
54
+
55
+ // ==========================================================================
56
+ // WebSocket for Events
57
+ // ==========================================================================
58
+
59
+ const eventHandlers = {
60
+ 'files-update': [],
61
+ 'venv-found': [],
62
+ 'venv-scan-done': [],
63
+ 'project:changed': [],
64
+ 'sync-server-died': [],
65
+ };
66
+
67
+ let ws = null;
68
+ let wsReconnectTimer = null;
69
+
70
+ function connectWebSocket() {
71
+ const wsUrl = new URL('/events', BASE_URL);
72
+ wsUrl.protocol = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:';
73
+ if (TOKEN) {
74
+ wsUrl.searchParams.set('token', TOKEN);
75
+ }
76
+
77
+ ws = new WebSocket(wsUrl.toString());
78
+
79
+ ws.onopen = () => {
80
+ console.log('[http-shim] WebSocket connected');
81
+ if (wsReconnectTimer) {
82
+ clearTimeout(wsReconnectTimer);
83
+ wsReconnectTimer = null;
84
+ }
85
+ };
86
+
87
+ ws.onmessage = (e) => {
88
+ try {
89
+ const { event, data } = JSON.parse(e.data);
90
+ const handlers = eventHandlers[event];
91
+ if (handlers) {
92
+ handlers.forEach(cb => {
93
+ try {
94
+ cb(data);
95
+ } catch (err) {
96
+ console.error('[http-shim] Event handler error:', err);
97
+ }
98
+ });
99
+ }
100
+ } catch (err) {
101
+ console.error('[http-shim] WebSocket message error:', err);
102
+ }
103
+ };
104
+
105
+ ws.onclose = () => {
106
+ console.log('[http-shim] WebSocket disconnected, reconnecting in 2s...');
107
+ wsReconnectTimer = setTimeout(connectWebSocket, 2000);
108
+ };
109
+
110
+ ws.onerror = (err) => {
111
+ console.error('[http-shim] WebSocket error:', err);
112
+ };
113
+ }
114
+
115
+ // Connect WebSocket on load
116
+ connectWebSocket();
117
+
118
+ // ==========================================================================
119
+ // electronAPI Shim
120
+ // ==========================================================================
121
+
122
+ window.electronAPI = {
123
+ // ========================================================================
124
+ // System
125
+ // ========================================================================
126
+
127
+ getHomeDir: () => GET('/api/system/home').then(r => r.homeDir),
128
+
129
+ getRecent: () => GET('/api/system/recent'),
130
+
131
+ getAi: () => GET('/api/system/ai'),
132
+
133
+ // ========================================================================
134
+ // Shell (stubs for browser)
135
+ // ========================================================================
136
+
137
+ shell: {
138
+ showItemInFolder: async (fullPath) => {
139
+ console.log('[http-shim] showItemInFolder not available in browser:', fullPath);
140
+ // Could show a toast with the path
141
+ return { success: false, path: fullPath };
142
+ },
143
+
144
+ openExternal: async (url) => {
145
+ window.open(url, '_blank');
146
+ return { success: true };
147
+ },
148
+
149
+ openPath: async (fullPath) => {
150
+ console.log('[http-shim] openPath not available in browser:', fullPath);
151
+ return { success: false, path: fullPath };
152
+ },
153
+ },
154
+
155
+ // ========================================================================
156
+ // File scanning
157
+ // ========================================================================
158
+
159
+ scanFiles: (searchDir) => GET(`/api/file/scan?root=${encodeURIComponent(searchDir || '')}`),
160
+
161
+ onFilesUpdate: (callback) => {
162
+ eventHandlers['files-update'].push(callback);
163
+ },
164
+
165
+ // ========================================================================
166
+ // Venv discovery
167
+ // ========================================================================
168
+
169
+ discoverVenvs: (projectDir) => POST('/api/system/discover-venvs', { projectDir }),
170
+
171
+ onVenvFound: (callback) => {
172
+ eventHandlers['venv-found'].push(callback);
173
+ },
174
+
175
+ onVenvScanDone: (callback) => {
176
+ eventHandlers['venv-scan-done'].push(callback);
177
+ },
178
+
179
+ // ========================================================================
180
+ // File info
181
+ // ========================================================================
182
+
183
+ readPreview: (filePath, lines) =>
184
+ GET(`/api/file/preview?path=${encodeURIComponent(filePath)}&lines=${lines || 40}`)
185
+ .then(r => r.content),
186
+
187
+ getFileInfo: (filePath) =>
188
+ GET(`/api/file/info?path=${encodeURIComponent(filePath)}`),
189
+
190
+ // ========================================================================
191
+ // Python management
192
+ // ========================================================================
193
+
194
+ installMrmdPython: (venvPath) =>
195
+ POST('/api/system/install-mrmd-python', { venvPath }),
196
+
197
+ startPython: (venvPath, forceNew = false) =>
198
+ POST('/api/runtime/start-python', { venvPath, forceNew }),
199
+
200
+ // ========================================================================
201
+ // Runtime management
202
+ // ========================================================================
203
+
204
+ listRuntimes: () => GET('/api/runtime'),
205
+
206
+ killRuntime: (runtimeId) => DELETE(`/api/runtime/${encodeURIComponent(runtimeId)}`),
207
+
208
+ attachRuntime: (runtimeId) => POST(`/api/runtime/${encodeURIComponent(runtimeId)}/attach`, {}),
209
+
210
+ // ========================================================================
211
+ // Open file (legacy)
212
+ // ========================================================================
213
+
214
+ openFile: async (filePath) => {
215
+ // This was used to open a file and get session info
216
+ // We'll get project info and session info separately
217
+ const project = await window.electronAPI.project.get(filePath);
218
+ const session = await window.electronAPI.session.forDocument(filePath);
219
+ return { project, session };
220
+ },
221
+
222
+ // ========================================================================
223
+ // PROJECT SERVICE
224
+ // ========================================================================
225
+
226
+ project: {
227
+ get: (filePath) =>
228
+ GET(`/api/project?path=${encodeURIComponent(filePath)}`),
229
+
230
+ create: (targetPath) =>
231
+ POST('/api/project', { targetPath }),
232
+
233
+ nav: (projectRoot) =>
234
+ GET(`/api/project/nav?root=${encodeURIComponent(projectRoot)}`),
235
+
236
+ invalidate: (projectRoot) =>
237
+ POST('/api/project/invalidate', { projectRoot }),
238
+
239
+ watch: (projectRoot) =>
240
+ POST('/api/project/watch', { projectRoot }),
241
+
242
+ unwatch: () =>
243
+ POST('/api/project/unwatch', {}),
244
+
245
+ onChanged: (callback) => {
246
+ // Remove existing handlers to prevent duplicates (matches Electron behavior)
247
+ eventHandlers['project:changed'] = [callback];
248
+ },
249
+ },
250
+
251
+ // ========================================================================
252
+ // SESSION SERVICE
253
+ // ========================================================================
254
+
255
+ session: {
256
+ list: () => GET('/api/session'),
257
+
258
+ start: (config) => POST('/api/session', { config }),
259
+
260
+ stop: (sessionName) => DELETE(`/api/session/${encodeURIComponent(sessionName)}`),
261
+
262
+ restart: (sessionName) => POST(`/api/session/${encodeURIComponent(sessionName)}/restart`, {}),
263
+
264
+ forDocument: (documentPath) => POST('/api/session/for-document', { documentPath }),
265
+ },
266
+
267
+ // ========================================================================
268
+ // BASH SESSION SERVICE
269
+ // ========================================================================
270
+
271
+ bash: {
272
+ list: () => GET('/api/bash'),
273
+
274
+ start: (config) => POST('/api/bash', { config }),
275
+
276
+ stop: (sessionName) => DELETE(`/api/bash/${encodeURIComponent(sessionName)}`),
277
+
278
+ restart: (sessionName) => POST(`/api/bash/${encodeURIComponent(sessionName)}/restart`, {}),
279
+
280
+ forDocument: (documentPath) => POST('/api/bash/for-document', { documentPath }),
281
+ },
282
+
283
+ // ========================================================================
284
+ // FILE SERVICE
285
+ // ========================================================================
286
+
287
+ file: {
288
+ scan: (root, options = {}) => {
289
+ const params = new URLSearchParams();
290
+ if (root) params.set('root', root);
291
+ if (options.extensions) params.set('extensions', options.extensions.join(','));
292
+ if (options.maxDepth) params.set('maxDepth', options.maxDepth);
293
+ if (options.includeHidden) params.set('includeHidden', 'true');
294
+ return GET(`/api/file/scan?${params.toString()}`);
295
+ },
296
+
297
+ create: (filePath, content = '') =>
298
+ POST('/api/file/create', { filePath, content }),
299
+
300
+ createInProject: (projectRoot, relativePath, content = '') =>
301
+ POST('/api/file/create-in-project', { projectRoot, relativePath, content }),
302
+
303
+ move: (projectRoot, fromPath, toPath) =>
304
+ POST('/api/file/move', { projectRoot, fromPath, toPath }),
305
+
306
+ reorder: (projectRoot, sourcePath, targetPath, position) =>
307
+ POST('/api/file/reorder', { projectRoot, sourcePath, targetPath, position }),
308
+
309
+ delete: (filePath) =>
310
+ DELETE(`/api/file?path=${encodeURIComponent(filePath)}`),
311
+
312
+ read: (filePath) =>
313
+ GET(`/api/file/read?path=${encodeURIComponent(filePath)}`),
314
+
315
+ write: (filePath, content) =>
316
+ POST('/api/file/write', { filePath, content }),
317
+ },
318
+
319
+ // ========================================================================
320
+ // ASSET SERVICE
321
+ // ========================================================================
322
+
323
+ asset: {
324
+ list: (projectRoot) =>
325
+ GET(`/api/asset?projectRoot=${encodeURIComponent(projectRoot || '')}`),
326
+
327
+ save: async (projectRoot, file, filename) => {
328
+ // Convert Uint8Array to base64 for JSON transport
329
+ const base64 = btoa(String.fromCharCode.apply(null, file));
330
+ return POST('/api/asset/save', {
331
+ projectRoot,
332
+ file: base64,
333
+ filename,
334
+ });
335
+ },
336
+
337
+ relativePath: (assetPath, documentPath) =>
338
+ GET(`/api/asset/relative-path?assetPath=${encodeURIComponent(assetPath)}&documentPath=${encodeURIComponent(documentPath)}`)
339
+ .then(r => r.relativePath),
340
+
341
+ orphans: (projectRoot) =>
342
+ GET(`/api/asset/orphans?projectRoot=${encodeURIComponent(projectRoot || '')}`),
343
+
344
+ delete: (projectRoot, assetPath) =>
345
+ DELETE(`/api/asset?projectRoot=${encodeURIComponent(projectRoot || '')}&assetPath=${encodeURIComponent(assetPath)}`),
346
+ },
347
+
348
+ // ========================================================================
349
+ // DATA LOSS PREVENTION
350
+ // ========================================================================
351
+
352
+ onSyncServerDied: (callback) => {
353
+ // Remove existing handlers to prevent duplicates
354
+ eventHandlers['sync-server-died'] = [callback];
355
+ },
356
+ };
357
+
358
+ // ==========================================================================
359
+ // Expose utilities
360
+ // ==========================================================================
361
+
362
+ window.MRMD_HTTP_SHIM = {
363
+ BASE_URL,
364
+ TOKEN,
365
+ reconnectWebSocket: connectWebSocket,
366
+ getWebSocketState: () => ws ? ws.readyState : -1,
367
+ };
368
+
369
+ console.log('[http-shim] electronAPI shim loaded', { BASE_URL, hasToken: !!TOKEN });
370
+
371
+ })();
@@ -0,0 +1,171 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>mrmd</title>
7
+
8
+ <!-- Load the HTTP shim BEFORE anything else -->
9
+ <script src="/http-shim.js"></script>
10
+
11
+ <style>
12
+ * { margin: 0; padding: 0; box-sizing: border-box; }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
16
+ background: #0d1117;
17
+ color: #c9d1d9;
18
+ height: 100vh;
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: center;
22
+ }
23
+
24
+ .loading {
25
+ text-align: center;
26
+ }
27
+
28
+ .loading h1 {
29
+ font-size: 2rem;
30
+ margin-bottom: 1rem;
31
+ color: #58a6ff;
32
+ }
33
+
34
+ .loading p {
35
+ color: #8b949e;
36
+ }
37
+
38
+ .spinner {
39
+ width: 40px;
40
+ height: 40px;
41
+ border: 3px solid #30363d;
42
+ border-top-color: #58a6ff;
43
+ border-radius: 50%;
44
+ animation: spin 1s linear infinite;
45
+ margin: 1rem auto;
46
+ }
47
+
48
+ @keyframes spin {
49
+ to { transform: rotate(360deg); }
50
+ }
51
+
52
+ .error {
53
+ color: #f85149;
54
+ margin-top: 1rem;
55
+ }
56
+ </style>
57
+ </head>
58
+ <body>
59
+ <div class="loading" id="loading">
60
+ <h1>mrmd</h1>
61
+ <div class="spinner"></div>
62
+ <p>Connecting to server...</p>
63
+ <p class="error" id="error" style="display: none;"></p>
64
+ </div>
65
+
66
+ <script>
67
+ // Validate token and load the full UI
68
+ async function init() {
69
+ const params = new URLSearchParams(window.location.search);
70
+ const token = params.get('token');
71
+
72
+ if (!token) {
73
+ showError('No token provided. Add ?token=YOUR_TOKEN to the URL.');
74
+ return;
75
+ }
76
+
77
+ try {
78
+ // Validate token
79
+ const response = await fetch(`/auth/validate?token=${encodeURIComponent(token)}`);
80
+ const result = await response.json();
81
+
82
+ if (!result.valid) {
83
+ showError('Invalid token. Check your access URL.');
84
+ return;
85
+ }
86
+
87
+ // Token is valid - load the full UI
88
+ // We'll redirect to the main app or load it inline
89
+ loadMainApp();
90
+
91
+ } catch (err) {
92
+ showError(`Connection failed: ${err.message}`);
93
+ }
94
+ }
95
+
96
+ function showError(message) {
97
+ document.getElementById('error').textContent = message;
98
+ document.getElementById('error').style.display = 'block';
99
+ }
100
+
101
+ async function loadMainApp() {
102
+ // For now, show a simple UI
103
+ // In the full implementation, this would load the mrmd-electron index.html content
104
+ // or use an iframe
105
+
106
+ document.body.innerHTML = `
107
+ <div style="padding: 20px; max-width: 800px; margin: 0 auto;">
108
+ <h1 style="color: #58a6ff; margin-bottom: 20px;">mrmd-server</h1>
109
+ <p style="margin-bottom: 20px; color: #8b949e;">
110
+ Server is running. The full UI will be loaded from mrmd-electron.
111
+ </p>
112
+
113
+ <div style="background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 20px;">
114
+ <h3 style="margin-bottom: 10px;">Quick Start</h3>
115
+ <p style="color: #8b949e; margin-bottom: 15px;">
116
+ To use the full mrmd editor, ensure mrmd-electron is available and the server
117
+ is configured to serve its index.html.
118
+ </p>
119
+
120
+ <h4 style="margin-top: 20px; margin-bottom: 10px;">API Status</h4>
121
+ <pre id="status" style="background: #0d1117; padding: 15px; border-radius: 4px; overflow: auto;">Loading...</pre>
122
+ </div>
123
+
124
+ <div style="margin-top: 20px; background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 20px;">
125
+ <h3 style="margin-bottom: 10px;">Test electronAPI</h3>
126
+ <button onclick="testAPI()" style="background: #238636; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer;">
127
+ Test API
128
+ </button>
129
+ <pre id="test-result" style="background: #0d1117; padding: 15px; border-radius: 4px; overflow: auto; margin-top: 15px; display: none;"></pre>
130
+ </div>
131
+ </div>
132
+ `;
133
+
134
+ // Load status
135
+ try {
136
+ const homeDir = await window.electronAPI.getHomeDir();
137
+ const recent = await window.electronAPI.getRecent();
138
+
139
+ document.getElementById('status').textContent = JSON.stringify({
140
+ connected: true,
141
+ homeDir,
142
+ recentFiles: recent.files?.length || 0,
143
+ recentVenvs: recent.venvs?.length || 0,
144
+ }, null, 2);
145
+ } catch (err) {
146
+ document.getElementById('status').textContent = `Error: ${err.message}`;
147
+ }
148
+ }
149
+
150
+ window.testAPI = async function() {
151
+ const resultEl = document.getElementById('test-result');
152
+ resultEl.style.display = 'block';
153
+ resultEl.textContent = 'Testing...';
154
+
155
+ try {
156
+ const results = {
157
+ homeDir: await window.electronAPI.getHomeDir(),
158
+ recent: await window.electronAPI.getRecent(),
159
+ ai: await window.electronAPI.getAi(),
160
+ };
161
+
162
+ resultEl.textContent = JSON.stringify(results, null, 2);
163
+ } catch (err) {
164
+ resultEl.textContent = `Error: ${err.message}`;
165
+ }
166
+ };
167
+
168
+ init();
169
+ </script>
170
+ </body>
171
+ </html>