mrmd-server 0.1.13 → 0.1.15

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.13",
3
+ "version": "0.1.15",
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/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
  /**
@@ -309,11 +312,12 @@ export async function createServer(config) {
309
312
  return;
310
313
  }
311
314
 
312
- // Handle /sync/:port/:doc - proxy to local sync server
315
+ // Handle /sync/:port/:path - proxy to local server (sync, pty, etc.)
313
316
  const syncMatch = url.pathname.match(/^\/sync\/(\d+)\/(.+)$/);
314
317
  if (syncMatch) {
315
- const [, syncPort, docName] = syncMatch;
316
- const targetUrl = `ws://127.0.0.1:${syncPort}/${docName}`;
318
+ const [, syncPort, pathPart] = syncMatch;
319
+ // Preserve query string for PTY sessions
320
+ const targetUrl = `ws://127.0.0.1:${syncPort}/${pathPart}${url.search}`;
317
321
 
318
322
  // Create connection to local sync server
319
323
  const upstream = new WsClient(targetUrl);
@@ -388,6 +392,9 @@ export async function createServer(config) {
388
392
  // Stop all sync servers
389
393
  stopAllSyncServers();
390
394
 
395
+ // Stop AI server
396
+ stopAiServer();
397
+
391
398
  // Stop all sessions via services (if they have shutdown methods)
392
399
  try {
393
400
  if (typeof sessionService.shutdown === 'function') {