mrmd-server 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mrmd-server",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "HTTP server for mrmd - run mrmd in any browser, access from anywhere",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -0,0 +1,184 @@
1
+ /**
2
+ * AI Service - manages the mrmd-ai server
3
+ *
4
+ * The AI server is shared across all sessions (stateless).
5
+ * It's started once on first request and kept running.
6
+ */
7
+
8
+ import { spawn, execSync } from 'child_process';
9
+ import net from 'net';
10
+ import path from 'path';
11
+ import { existsSync } from 'fs';
12
+ import os from 'os';
13
+
14
+ // AI server singleton
15
+ let aiServer = null;
16
+ let startPromise = null;
17
+
18
+ /**
19
+ * Find a free port
20
+ */
21
+ async function findFreePort() {
22
+ return new Promise((resolve, reject) => {
23
+ const srv = net.createServer();
24
+ srv.listen(0, () => {
25
+ const { port } = srv.address();
26
+ srv.close(() => resolve(port));
27
+ });
28
+ srv.on('error', reject);
29
+ });
30
+ }
31
+
32
+ /**
33
+ * Wait for a port to be available (server started)
34
+ */
35
+ async function waitForPort(port, { timeout = 30000 } = {}) {
36
+ const start = Date.now();
37
+ while (Date.now() - start < timeout) {
38
+ try {
39
+ await new Promise((resolve, reject) => {
40
+ const socket = net.connect(port, '127.0.0.1');
41
+ socket.on('connect', () => {
42
+ socket.destroy();
43
+ resolve();
44
+ });
45
+ socket.on('error', reject);
46
+ });
47
+ return true;
48
+ } catch {
49
+ await new Promise(r => setTimeout(r, 200));
50
+ }
51
+ }
52
+ throw new Error(`Timeout waiting for port ${port}`);
53
+ }
54
+
55
+ /**
56
+ * Find uv executable
57
+ */
58
+ function findUv() {
59
+ try {
60
+ return execSync('which uv', { encoding: 'utf-8' }).trim();
61
+ } catch {
62
+ // Check common locations
63
+ const locations = [
64
+ path.join(os.homedir(), '.local', 'bin', 'uv'),
65
+ '/usr/local/bin/uv',
66
+ '/usr/bin/uv',
67
+ path.join(os.homedir(), '.cargo', 'bin', 'uv'),
68
+ ];
69
+ for (const loc of locations) {
70
+ if (existsSync(loc)) {
71
+ return loc;
72
+ }
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+
78
+ /**
79
+ * Ensure AI server is running
80
+ * Returns { port, url, success } or { error, success: false }
81
+ */
82
+ export async function ensureAiServer() {
83
+ // Already running
84
+ if (aiServer) {
85
+ return {
86
+ success: true,
87
+ port: aiServer.port,
88
+ url: `http://localhost:${aiServer.port}`,
89
+ };
90
+ }
91
+
92
+ // Already starting (avoid race condition)
93
+ if (startPromise) {
94
+ return startPromise;
95
+ }
96
+
97
+ startPromise = (async () => {
98
+ const uvPath = findUv();
99
+ if (!uvPath) {
100
+ return {
101
+ success: false,
102
+ error: "'uv' is not installed. Install it with: curl -LsSf https://astral.sh/uv/install.sh | sh",
103
+ };
104
+ }
105
+
106
+ const port = await findFreePort();
107
+ console.log(`[ai] Starting mrmd-ai on port ${port}...`);
108
+
109
+ const proc = spawn(uvPath, [
110
+ 'tool', 'run',
111
+ '--from', 'mrmd-ai>=0.1.0,<0.2',
112
+ 'mrmd-ai-server',
113
+ '--port', port.toString(),
114
+ ], {
115
+ stdio: ['pipe', 'pipe', 'pipe'],
116
+ });
117
+
118
+ proc.stdout.on('data', (d) => console.log('[ai]', d.toString().trim()));
119
+ proc.stderr.on('data', (d) => console.error('[ai]', d.toString().trim()));
120
+ proc.on('exit', (code) => {
121
+ console.log(`[ai] AI server exited with code ${code}`);
122
+ aiServer = null;
123
+ startPromise = null;
124
+ });
125
+
126
+ try {
127
+ // AI server imports heavy libs (dspy, litellm) - needs 30s timeout
128
+ await waitForPort(port, { timeout: 30000 });
129
+
130
+ aiServer = { proc, port };
131
+ console.log(`[ai] AI server ready on port ${port}`);
132
+
133
+ return {
134
+ success: true,
135
+ port,
136
+ url: `http://localhost:${port}`,
137
+ };
138
+ } catch (e) {
139
+ proc.kill('SIGTERM');
140
+ startPromise = null;
141
+ return {
142
+ success: false,
143
+ error: `AI server failed to start: ${e.message}`,
144
+ };
145
+ }
146
+ })();
147
+
148
+ const result = await startPromise;
149
+ if (!result.success) {
150
+ startPromise = null;
151
+ }
152
+ return result;
153
+ }
154
+
155
+ /**
156
+ * Get current AI server status
157
+ */
158
+ export function getAiServer() {
159
+ if (aiServer) {
160
+ return {
161
+ success: true,
162
+ port: aiServer.port,
163
+ url: `http://localhost:${aiServer.port}`,
164
+ running: true,
165
+ };
166
+ }
167
+ return {
168
+ success: false,
169
+ running: false,
170
+ error: 'AI server not started',
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Stop AI server
176
+ */
177
+ export function stopAiServer() {
178
+ if (aiServer?.proc) {
179
+ console.log('[ai] Stopping AI server...');
180
+ aiServer.proc.kill('SIGTERM');
181
+ aiServer = null;
182
+ startPromise = null;
183
+ }
184
+ }
package/src/api/file.js CHANGED
@@ -22,10 +22,13 @@ export function createFileRoutes(ctx) {
22
22
  */
23
23
  router.get('/scan', async (req, res) => {
24
24
  try {
25
- const root = req.query.root || ctx.projectDir;
25
+ // Default to home directory (like Electron's file picker)
26
+ const os = await import('os');
27
+ const root = req.query.root || ctx.projectDir || os.default.homedir();
26
28
  const options = {
27
- extensions: req.query.extensions?.split(',') || ['.md'],
28
- maxDepth: parseInt(req.query.maxDepth) || 6,
29
+ // Default to both .md and .ipynb (like Electron)
30
+ extensions: req.query.extensions?.split(',') || ['.md', '.ipynb'],
31
+ maxDepth: parseInt(req.query.maxDepth) || 10,
29
32
  includeHidden: req.query.includeHidden === 'true',
30
33
  };
31
34
 
package/src/api/system.js CHANGED
@@ -10,6 +10,7 @@ import path from 'path';
10
10
  import fs from 'fs/promises';
11
11
  import { existsSync } from 'fs';
12
12
  import { spawn, execSync } from 'child_process';
13
+ import { ensureAiServer, getAiServer } from '../ai-service.js';
13
14
 
14
15
  /**
15
16
  * Create system routes
@@ -187,14 +188,26 @@ export function createSystemRoutes(ctx) {
187
188
 
188
189
  /**
189
190
  * GET /api/system/ai
190
- * Get AI server info
191
+ * Get AI server info - ensures AI server is running
191
192
  * Mirrors: electronAPI.getAi()
192
193
  */
193
- router.get('/ai', (req, res) => {
194
- res.json({
195
- port: ctx.aiPort,
196
- url: `http://localhost:${ctx.aiPort}`,
197
- });
194
+ router.get('/ai', async (req, res) => {
195
+ try {
196
+ // Ensure AI server is running (starts it if not)
197
+ const result = await ensureAiServer();
198
+ res.json(result);
199
+ } catch (err) {
200
+ console.error('[system:ai]', err);
201
+ res.status(500).json({ success: false, error: err.message });
202
+ }
203
+ });
204
+
205
+ /**
206
+ * GET /api/system/ai/status
207
+ * Get AI server status without starting it
208
+ */
209
+ router.get('/ai/status', (req, res) => {
210
+ res.json(getAiServer());
198
211
  });
199
212
 
200
213
  /**
package/src/server.js CHANGED
@@ -55,6 +55,9 @@ import {
55
55
  onSyncDeath,
56
56
  } from './sync-manager.js';
57
57
 
58
+ // Import AI service for mrmd-ai server management
59
+ import { stopAiServer } from './ai-service.js';
60
+
58
61
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
59
62
 
60
63
  /**
@@ -321,9 +324,13 @@ export async function createServer(config) {
321
324
 
322
325
  upstream.on('open', () => {
323
326
  syncWss.handleUpgrade(request, socket, head, (clientWs) => {
324
- // Bidirectional proxy
325
- clientWs.on('message', (data) => upstream.send(data));
326
- upstream.on('message', (data) => clientWs.send(data));
327
+ // Bidirectional proxy - preserve message type (binary/text)
328
+ clientWs.on('message', (data, isBinary) => {
329
+ upstream.send(data, { binary: isBinary });
330
+ });
331
+ upstream.on('message', (data, isBinary) => {
332
+ clientWs.send(data, { binary: isBinary });
333
+ });
327
334
  clientWs.on('close', () => upstream.close());
328
335
  upstream.on('close', () => clientWs.close());
329
336
  clientWs.on('error', () => upstream.close());
@@ -389,6 +396,9 @@ export async function createServer(config) {
389
396
  // Stop all sync servers
390
397
  stopAllSyncServers();
391
398
 
399
+ // Stop AI server
400
+ stopAiServer();
401
+
392
402
  // Stop all sessions via services (if they have shutdown methods)
393
403
  try {
394
404
  if (typeof sessionService.shutdown === 'function') {