mrmd-server 0.2.4 → 0.2.6
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/api/file.js +137 -0
- package/src/api/index.js +1 -0
- package/src/api/languagetool.js +119 -0
- package/src/api/project.js +95 -108
- package/src/api/runtime.js +204 -64
- package/src/api/settings.js +4 -4
- package/src/api/voice.js +406 -0
- package/src/cloud-seed.js +359 -0
- package/src/cloud-session-service.js +0 -85
- package/src/relay-bridge.js +301 -0
- package/src/runtime-tunnel-client.js +773 -0
- package/src/server.js +284 -1
- package/src/services.js +12 -0
- package/src/sync-manager.js +91 -0
- package/static/http-shim.js +112 -84
package/src/api/runtime.js
CHANGED
|
@@ -6,34 +6,161 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { Router } from 'express';
|
|
9
|
-
import { Project } from 'mrmd-project';
|
|
10
9
|
import fs from 'fs';
|
|
11
10
|
import path from 'path';
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
|
-
*
|
|
13
|
+
* Resolve project root from a file path without requiring mrmd.md.
|
|
15
14
|
*/
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
function findGitRoot(startDir) {
|
|
16
|
+
let current = path.resolve(startDir || '/');
|
|
17
|
+
while (true) {
|
|
18
|
+
if (fs.existsSync(path.join(current, '.git'))) return current;
|
|
19
|
+
const parent = path.dirname(current);
|
|
20
|
+
if (!parent || parent === current) break;
|
|
21
|
+
current = parent;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
19
25
|
|
|
26
|
+
function hasDocsInDir(dir) {
|
|
20
27
|
try {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const config = Project.parseConfig(content);
|
|
24
|
-
return { root, config };
|
|
28
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
29
|
+
return entries.some((entry) => entry.isFile() && /\.(md|qmd)$/i.test(entry.name));
|
|
25
30
|
} catch {
|
|
26
|
-
return
|
|
31
|
+
return false;
|
|
27
32
|
}
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
function resolveProjectRoot(filePath, fallbackRoot = null) {
|
|
36
|
+
const abs = path.resolve(String(filePath || fallbackRoot || process.cwd()));
|
|
37
|
+
let startDir = abs;
|
|
38
|
+
try {
|
|
39
|
+
const stat = fs.statSync(abs);
|
|
40
|
+
if (stat.isFile()) startDir = path.dirname(abs);
|
|
41
|
+
} catch {
|
|
42
|
+
if (path.extname(abs)) startDir = path.dirname(abs);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const gitRoot = findGitRoot(startDir);
|
|
46
|
+
if (gitRoot) return gitRoot;
|
|
47
|
+
|
|
48
|
+
const homeDir = path.resolve(process.env.HOME || '/');
|
|
49
|
+
let current = startDir;
|
|
50
|
+
let candidate = startDir;
|
|
51
|
+
for (let i = 0; i < 4; i++) {
|
|
52
|
+
const parent = path.dirname(current);
|
|
53
|
+
if (!parent || parent === current || parent === '/' || parent === homeDir) break;
|
|
54
|
+
if (hasDocsInDir(parent)) {
|
|
55
|
+
candidate = parent;
|
|
56
|
+
current = parent;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return candidate;
|
|
63
|
+
}
|
|
64
|
+
|
|
30
65
|
/**
|
|
31
66
|
* Create unified runtime routes.
|
|
32
67
|
* @param {Object} ctx — server context with runtimeService
|
|
33
68
|
*/
|
|
34
69
|
export function createRuntimeRoutes(ctx) {
|
|
35
70
|
const router = Router();
|
|
36
|
-
const { runtimeService } = ctx;
|
|
71
|
+
const { runtimeService, runtimePreferencesService } = ctx;
|
|
72
|
+
|
|
73
|
+
function normalizeRuntimeLanguage(language) {
|
|
74
|
+
const l = String(language || '').toLowerCase();
|
|
75
|
+
if (l === 'py' || l === 'python3') return 'python';
|
|
76
|
+
if (l === 'sh' || l === 'shell' || l === 'zsh') return 'bash';
|
|
77
|
+
if (l === 'rlang') return 'r';
|
|
78
|
+
if (l === 'jl') return 'julia';
|
|
79
|
+
if (l === 'term' || l === 'terminal') return 'pty';
|
|
80
|
+
return l;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function ensureEffectiveRuntime(documentPath, language, options = {}) {
|
|
84
|
+
const normalized = normalizeRuntimeLanguage(language);
|
|
85
|
+
const supported = new Set(runtimeService.supportedLanguages());
|
|
86
|
+
if (!supported.has(normalized)) {
|
|
87
|
+
return {
|
|
88
|
+
language: normalized,
|
|
89
|
+
alive: false,
|
|
90
|
+
available: false,
|
|
91
|
+
error: `Unsupported runtime language: ${language}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const resolvedProjectRoot = resolveProjectRoot(documentPath, options.projectRoot || ctx.projectDir || process.cwd());
|
|
96
|
+
|
|
97
|
+
let effective = null;
|
|
98
|
+
let startConfig = null;
|
|
99
|
+
if (runtimePreferencesService?.getEffectiveForDocument) {
|
|
100
|
+
effective = await runtimePreferencesService.getEffectiveForDocument({
|
|
101
|
+
documentPath,
|
|
102
|
+
language: normalized,
|
|
103
|
+
projectRoot: resolvedProjectRoot,
|
|
104
|
+
deviceKind: options.deviceKind || 'desktop',
|
|
105
|
+
});
|
|
106
|
+
startConfig = runtimePreferencesService.toRuntimeStartConfig(effective);
|
|
107
|
+
} else {
|
|
108
|
+
const projectName = path.basename(resolvedProjectRoot) || 'project';
|
|
109
|
+
startConfig = {
|
|
110
|
+
name: `rt:notebook:${projectName}:${normalized}`,
|
|
111
|
+
language: normalized,
|
|
112
|
+
cwd: resolvedProjectRoot,
|
|
113
|
+
};
|
|
114
|
+
if (normalized === 'python') {
|
|
115
|
+
startConfig.venv = path.join(resolvedProjectRoot, '.venv');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// If tunnel provider is available, prefer it so execution runs on user's machine.
|
|
120
|
+
if (ctx.tunnelClient?.isAvailable()) {
|
|
121
|
+
try {
|
|
122
|
+
const tunnelResult = await ctx.tunnelClient.startRuntime({
|
|
123
|
+
language: normalized,
|
|
124
|
+
documentPath,
|
|
125
|
+
projectRoot: resolvedProjectRoot,
|
|
126
|
+
});
|
|
127
|
+
const langResult = tunnelResult?.[normalized];
|
|
128
|
+
if (langResult) {
|
|
129
|
+
return {
|
|
130
|
+
...langResult,
|
|
131
|
+
id: langResult.name,
|
|
132
|
+
alive: !!langResult.alive,
|
|
133
|
+
available: true,
|
|
134
|
+
effective,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.warn(`[runtime:ensure:${normalized}] Tunnel failed, falling back to local:`, err.message);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const runtime = await runtimeService.start(startConfig);
|
|
144
|
+
return {
|
|
145
|
+
...runtime,
|
|
146
|
+
id: runtime.name,
|
|
147
|
+
alive: true,
|
|
148
|
+
available: true,
|
|
149
|
+
autoStart: true,
|
|
150
|
+
effective,
|
|
151
|
+
};
|
|
152
|
+
} catch (e) {
|
|
153
|
+
return {
|
|
154
|
+
...startConfig,
|
|
155
|
+
id: startConfig.name,
|
|
156
|
+
alive: false,
|
|
157
|
+
available: true,
|
|
158
|
+
autoStart: true,
|
|
159
|
+
error: e.message,
|
|
160
|
+
effective,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
37
164
|
|
|
38
165
|
/**
|
|
39
166
|
* GET /api/runtime
|
|
@@ -42,6 +169,15 @@ export function createRuntimeRoutes(ctx) {
|
|
|
42
169
|
*/
|
|
43
170
|
router.get('/', async (req, res) => {
|
|
44
171
|
try {
|
|
172
|
+
if (ctx.tunnelClient?.isAvailable()) {
|
|
173
|
+
try {
|
|
174
|
+
const tunnelRuntimes = await ctx.tunnelClient.listRuntimes(req.query.language);
|
|
175
|
+
console.log('[runtime:list] Using tunnel — listing Electron runtimes');
|
|
176
|
+
return res.json(tunnelRuntimes);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.warn('[runtime:list] Tunnel list failed, falling back to local:', err.message);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
45
181
|
res.json(runtimeService.list(req.query.language));
|
|
46
182
|
} catch (err) {
|
|
47
183
|
console.error('[runtime:list]', err);
|
|
@@ -60,6 +196,15 @@ export function createRuntimeRoutes(ctx) {
|
|
|
60
196
|
if (!config?.name || !config?.language) {
|
|
61
197
|
return res.status(400).json({ error: 'config.name and config.language required' });
|
|
62
198
|
}
|
|
199
|
+
if (ctx.tunnelClient?.isAvailable()) {
|
|
200
|
+
try {
|
|
201
|
+
const tunnelResult = await ctx.tunnelClient.startRuntime(config);
|
|
202
|
+
console.log(`[runtime:start] Using tunnel — starting Electron runtime "${config.name}"`);
|
|
203
|
+
return res.json(tunnelResult?.[config.language] || tunnelResult);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
console.warn(`[runtime:start] Tunnel start failed for "${config.name}", falling back:`, err.message);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
63
208
|
const result = await runtimeService.start(config);
|
|
64
209
|
res.json(result);
|
|
65
210
|
} catch (err) {
|
|
@@ -74,6 +219,15 @@ export function createRuntimeRoutes(ctx) {
|
|
|
74
219
|
*/
|
|
75
220
|
router.delete('/:name', async (req, res) => {
|
|
76
221
|
try {
|
|
222
|
+
if (ctx.tunnelClient?.isAvailable()) {
|
|
223
|
+
try {
|
|
224
|
+
const result = await ctx.tunnelClient.stopRuntime(req.params.name);
|
|
225
|
+
console.log(`[runtime:stop] Using tunnel — stopping Electron runtime "${req.params.name}"`);
|
|
226
|
+
return res.json(result);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.warn(`[runtime:stop] Tunnel stop failed for "${req.params.name}", falling back:`, err.message);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
77
231
|
await runtimeService.stop(req.params.name);
|
|
78
232
|
res.json({ success: true });
|
|
79
233
|
} catch (err) {
|
|
@@ -88,6 +242,15 @@ export function createRuntimeRoutes(ctx) {
|
|
|
88
242
|
*/
|
|
89
243
|
router.post('/:name/restart', async (req, res) => {
|
|
90
244
|
try {
|
|
245
|
+
if (ctx.tunnelClient?.isAvailable()) {
|
|
246
|
+
try {
|
|
247
|
+
const result = await ctx.tunnelClient.restartRuntime(req.params.name);
|
|
248
|
+
console.log(`[runtime:restart] Using tunnel — restarting Electron runtime "${req.params.name}"`);
|
|
249
|
+
return res.json(result);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
console.warn(`[runtime:restart] Tunnel restart failed for "${req.params.name}", falling back:`, err.message);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
91
254
|
const result = await runtimeService.restart(req.params.name);
|
|
92
255
|
res.json(result);
|
|
93
256
|
} catch (err) {
|
|
@@ -116,43 +279,25 @@ export function createRuntimeRoutes(ctx) {
|
|
|
116
279
|
/**
|
|
117
280
|
* POST /api/runtime/for-document
|
|
118
281
|
* Get or create ALL runtimes needed for a document.
|
|
119
|
-
* Body: { documentPath,
|
|
120
|
-
* Returns: { python: {...}, bash: {...}, r: {...}, julia: {...}, pty: {...} }
|
|
282
|
+
* Body: { documentPath, projectRoot?, deviceKind? }
|
|
121
283
|
*/
|
|
122
284
|
router.post('/for-document', async (req, res) => {
|
|
123
285
|
try {
|
|
124
|
-
|
|
125
|
-
|
|
286
|
+
const { documentPath, projectRoot, deviceKind } = req.body;
|
|
126
287
|
if (!documentPath) {
|
|
127
288
|
return res.status(400).json({ error: 'documentPath required' });
|
|
128
289
|
}
|
|
129
290
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
projectRoot
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
projectRoot = projectRoot || (ctx.projectDir || process.cwd());
|
|
138
|
-
projectConfig = projectConfig || {};
|
|
139
|
-
}
|
|
291
|
+
const out = {};
|
|
292
|
+
const languages = runtimeService.supportedLanguages();
|
|
293
|
+
for (const language of languages) {
|
|
294
|
+
out[language] = await ensureEffectiveRuntime(documentPath, language, {
|
|
295
|
+
projectRoot,
|
|
296
|
+
deviceKind,
|
|
297
|
+
});
|
|
140
298
|
}
|
|
141
299
|
|
|
142
|
-
|
|
143
|
-
if (!frontmatter) {
|
|
144
|
-
try {
|
|
145
|
-
const content = fs.readFileSync(documentPath, 'utf8');
|
|
146
|
-
frontmatter = Project.parseFrontmatter(content);
|
|
147
|
-
} catch {
|
|
148
|
-
frontmatter = null;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const result = await runtimeService.getForDocument(
|
|
153
|
-
documentPath, projectConfig, frontmatter, projectRoot
|
|
154
|
-
);
|
|
155
|
-
res.json(result);
|
|
300
|
+
res.json(out);
|
|
156
301
|
} catch (err) {
|
|
157
302
|
console.error('[runtime:forDocument]', err);
|
|
158
303
|
res.json(null);
|
|
@@ -162,40 +307,21 @@ export function createRuntimeRoutes(ctx) {
|
|
|
162
307
|
/**
|
|
163
308
|
* POST /api/runtime/for-document/:language
|
|
164
309
|
* Get or create a runtime for a specific language.
|
|
165
|
-
* Body: { documentPath,
|
|
310
|
+
* Body: { documentPath, projectRoot?, deviceKind? }
|
|
166
311
|
*/
|
|
167
312
|
router.post('/for-document/:language', async (req, res) => {
|
|
168
313
|
try {
|
|
169
|
-
|
|
314
|
+
const { documentPath, projectRoot, deviceKind } = req.body;
|
|
170
315
|
const { language } = req.params;
|
|
171
316
|
|
|
172
317
|
if (!documentPath) {
|
|
173
318
|
return res.status(400).json({ error: 'documentPath required' });
|
|
174
319
|
}
|
|
175
320
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
projectConfig = projectConfig || detected.config;
|
|
181
|
-
} else {
|
|
182
|
-
projectRoot = projectRoot || (ctx.projectDir || process.cwd());
|
|
183
|
-
projectConfig = projectConfig || {};
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (!frontmatter) {
|
|
188
|
-
try {
|
|
189
|
-
const content = fs.readFileSync(documentPath, 'utf8');
|
|
190
|
-
frontmatter = Project.parseFrontmatter(content);
|
|
191
|
-
} catch {
|
|
192
|
-
frontmatter = null;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const result = await runtimeService.getForDocumentLanguage(
|
|
197
|
-
language, documentPath, projectConfig, frontmatter, projectRoot
|
|
198
|
-
);
|
|
321
|
+
const result = await ensureEffectiveRuntime(documentPath, language, {
|
|
322
|
+
projectRoot,
|
|
323
|
+
deviceKind,
|
|
324
|
+
});
|
|
199
325
|
res.json(result);
|
|
200
326
|
} catch (err) {
|
|
201
327
|
console.error('[runtime:forDocumentLanguage]', err);
|
|
@@ -211,6 +337,20 @@ export function createRuntimeRoutes(ctx) {
|
|
|
211
337
|
res.json(runtimeService.isAvailable(req.params.language));
|
|
212
338
|
});
|
|
213
339
|
|
|
340
|
+
/**
|
|
341
|
+
* GET /api/runtime/provider
|
|
342
|
+
* Return connected machine-provider info for tunnel mode.
|
|
343
|
+
*/
|
|
344
|
+
router.get('/provider', (req, res) => {
|
|
345
|
+
const provider = ctx.tunnelClient?.getProvider?.() || null;
|
|
346
|
+
const machineInfo = ctx.tunnelClient?.getMachines?.() || { activeMachineId: null, machines: [] };
|
|
347
|
+
res.json({
|
|
348
|
+
available: !!provider,
|
|
349
|
+
provider,
|
|
350
|
+
...machineInfo,
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
214
354
|
/**
|
|
215
355
|
* GET /api/runtime/languages
|
|
216
356
|
* List all supported languages.
|
package/src/api/settings.js
CHANGED
|
@@ -69,8 +69,8 @@ const DEFAULT_SETTINGS = {
|
|
|
69
69
|
|
|
70
70
|
// Default preferences
|
|
71
71
|
defaults: {
|
|
72
|
-
juiceLevel:
|
|
73
|
-
reasoningLevel:
|
|
72
|
+
juiceLevel: 1,
|
|
73
|
+
reasoningLevel: 0,
|
|
74
74
|
},
|
|
75
75
|
};
|
|
76
76
|
|
|
@@ -682,8 +682,8 @@ export function createSettingsRoutes(ctx) {
|
|
|
682
682
|
try {
|
|
683
683
|
const settings = loadSettings();
|
|
684
684
|
res.json({
|
|
685
|
-
juiceLevel: settings.defaults?.juiceLevel ??
|
|
686
|
-
reasoningLevel: settings.defaults?.reasoningLevel ??
|
|
685
|
+
juiceLevel: settings.defaults?.juiceLevel ?? 1,
|
|
686
|
+
reasoningLevel: settings.defaults?.reasoningLevel ?? 0,
|
|
687
687
|
});
|
|
688
688
|
} catch (err) {
|
|
689
689
|
console.error('[settings:getDefaults]', err);
|