mrmd-server 0.1.14 → 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.14",
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
  /**
@@ -389,6 +392,9 @@ export async function createServer(config) {
389
392
  // Stop all sync servers
390
393
  stopAllSyncServers();
391
394
 
395
+ // Stop AI server
396
+ stopAiServer();
397
+
392
398
  // Stop all sessions via services (if they have shutdown methods)
393
399
  try {
394
400
  if (typeof sessionService.shutdown === 'function') {