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 +1 -1
- package/src/ai-service.js +184 -0
- package/src/api/file.js +6 -3
- package/src/api/system.js +19 -6
- package/src/server.js +13 -3
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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) =>
|
|
326
|
-
|
|
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') {
|