mrmd-server 0.1.21 → 0.1.23
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/README.md +199 -116
- package/bin/cli.js +1 -1
- package/package.json +3 -3
- package/src/api/file.js +6 -6
- package/src/api/julia.js +97 -215
- package/src/api/project.js +22 -5
- package/src/api/r.js +102 -213
- package/src/server.js +45 -1
- package/static/http-shim.js +6 -2
package/README.md
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
#
|
|
1
|
+
# MRMD Server
|
|
2
2
|
|
|
3
|
-
Run
|
|
3
|
+
**Run MRMD in any browser.** Access your markdown notebooks from anywhere — your phone, tablet, or any machine with a browser.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/mrmd-server) [](LICENSE)
|
|
4
6
|
|
|
5
7
|
```
|
|
6
8
|
┌─────────────────────────────────────────────────────────────┐
|
|
7
|
-
│ Your VPS / Cloud
|
|
9
|
+
│ Your Server / VPS / Cloud │
|
|
8
10
|
│ │
|
|
9
11
|
│ ┌─────────────────────────────────────────────────────┐ │
|
|
10
12
|
│ │ mrmd-server │ │
|
|
11
|
-
│ │ •
|
|
12
|
-
│ │ •
|
|
13
|
-
│ │ •
|
|
14
|
-
│ │ • Token authentication
|
|
13
|
+
│ │ • Full MRMD UI served over HTTP │ │
|
|
14
|
+
│ │ • Code execution (Python, JS, Bash, R, Julia) │ │
|
|
15
|
+
│ │ • Real-time collaboration via WebSocket │ │
|
|
16
|
+
│ │ • Token-based authentication │ │
|
|
15
17
|
│ └─────────────────────────────────────────────────────┘ │
|
|
16
18
|
│ │
|
|
17
19
|
└─────────────────────────────────────────────────────────────┘
|
|
@@ -25,23 +27,65 @@ Run mrmd in any browser. Access your notebooks from anywhere.
|
|
|
25
27
|
└───────┘ └─────────┘ └─────────┘
|
|
26
28
|
```
|
|
27
29
|
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## What is MRMD Server?
|
|
33
|
+
|
|
34
|
+
MRMD Server is the headless/server version of [MRMD Electron](https://github.com/MaximeRivest/mrmd-electron). It provides the same markdown notebook experience without requiring a desktop app — just start the server and open it in any browser.
|
|
35
|
+
|
|
36
|
+
**Use cases:**
|
|
37
|
+
- Run notebooks on a remote GPU server, access from your laptop
|
|
38
|
+
- Host shared notebooks for a team
|
|
39
|
+
- Access your notebooks from your phone or tablet
|
|
40
|
+
- Deploy on a cloud VM for always-on compute
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
28
44
|
## Features
|
|
29
45
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- **Real-time collaboration** - Yjs sync works over WebSocket
|
|
33
|
-
- **Token authentication** - Secure access with shareable links
|
|
34
|
-
- **Portable compute** - Move your disk to a GPU server when needed
|
|
46
|
+
### Same UI as Desktop
|
|
47
|
+
The exact same editor, code execution, and collaboration features as MRMD Electron.
|
|
35
48
|
|
|
36
|
-
|
|
49
|
+
### Access from Anywhere
|
|
50
|
+
Open your notebooks in any browser — phone, tablet, another computer.
|
|
51
|
+
|
|
52
|
+
### Real-Time Collaboration
|
|
53
|
+
Share the URL with teammates. Changes sync instantly via Yjs CRDT.
|
|
54
|
+
|
|
55
|
+
### Token Authentication
|
|
56
|
+
Secure access with auto-generated or custom tokens. Share links safely.
|
|
57
|
+
|
|
58
|
+
### Portable Compute
|
|
59
|
+
Start the server wherever your data lives. GPU machine? Local workstation? Cloud VM? Just run `mrmd-server`.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
### npm (recommended)
|
|
37
66
|
|
|
38
67
|
```bash
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
npm install
|
|
68
|
+
npm install -g mrmd-server
|
|
69
|
+
```
|
|
42
70
|
|
|
43
|
-
|
|
71
|
+
### npx (no install)
|
|
72
|
+
|
|
73
|
+
```bash
|
|
44
74
|
npx mrmd-server ./my-notebooks
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Requirements
|
|
78
|
+
|
|
79
|
+
- **Node.js 18+**
|
|
80
|
+
- **Python 3.11+** with [uv](https://github.com/astral-sh/uv) (for Python execution)
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Quick Start
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Start server in your project directory
|
|
88
|
+
mrmd-server ./my-notebooks
|
|
45
89
|
|
|
46
90
|
# Output:
|
|
47
91
|
# mrmd-server
|
|
@@ -54,9 +98,26 @@ npx mrmd-server ./my-notebooks
|
|
|
54
98
|
# http://localhost:8080?token=abc123xyz...
|
|
55
99
|
```
|
|
56
100
|
|
|
101
|
+
Open the Access URL in your browser. That's it.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
57
105
|
## Usage
|
|
58
106
|
|
|
59
|
-
###
|
|
107
|
+
### Command Line Options
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
mrmd-server [options] [project-dir]
|
|
111
|
+
|
|
112
|
+
Options:
|
|
113
|
+
-p, --port <port> HTTP port (default: 8080)
|
|
114
|
+
-h, --host <host> Bind address (default: 0.0.0.0)
|
|
115
|
+
-t, --token <token> Auth token (auto-generated if not provided)
|
|
116
|
+
--no-auth Disable authentication (local dev only!)
|
|
117
|
+
--help Show help
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Examples
|
|
60
121
|
|
|
61
122
|
```bash
|
|
62
123
|
# Start in current directory
|
|
@@ -68,163 +129,185 @@ mrmd-server ./my-project
|
|
|
68
129
|
# Custom port
|
|
69
130
|
mrmd-server -p 3000 ./my-project
|
|
70
131
|
|
|
71
|
-
# With specific token
|
|
132
|
+
# With specific token (for automation)
|
|
72
133
|
mrmd-server -t my-secret-token ./my-project
|
|
73
134
|
|
|
74
135
|
# No auth (local development only!)
|
|
75
|
-
mrmd-server --no-auth
|
|
136
|
+
mrmd-server --no-auth
|
|
76
137
|
```
|
|
77
138
|
|
|
78
|
-
|
|
139
|
+
---
|
|
79
140
|
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
mrmd-server -p 8080 /home/you/notebooks
|
|
83
|
-
```
|
|
141
|
+
## Remote Access Setup
|
|
84
142
|
|
|
85
|
-
|
|
86
|
-
```nginx
|
|
87
|
-
server {
|
|
88
|
-
listen 443 ssl;
|
|
89
|
-
server_name notebooks.example.com;
|
|
143
|
+
### 1. Start the server on your remote machine
|
|
90
144
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
145
|
+
```bash
|
|
146
|
+
ssh your-server
|
|
147
|
+
cd /path/to/notebooks
|
|
148
|
+
mrmd-server -p 8080
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 2. Set up HTTPS with nginx (recommended)
|
|
152
|
+
|
|
153
|
+
```nginx
|
|
154
|
+
server {
|
|
155
|
+
listen 443 ssl;
|
|
156
|
+
server_name notebooks.example.com;
|
|
99
157
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
158
|
+
ssl_certificate /path/to/cert.pem;
|
|
159
|
+
ssl_certificate_key /path/to/key.pem;
|
|
160
|
+
|
|
161
|
+
location / {
|
|
162
|
+
proxy_pass http://localhost:8080;
|
|
163
|
+
proxy_http_version 1.1;
|
|
164
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
165
|
+
proxy_set_header Connection "upgrade";
|
|
166
|
+
proxy_set_header Host $host;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
104
170
|
|
|
105
|
-
###
|
|
171
|
+
### 3. Access from anywhere
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
https://notebooks.example.com?token=YOUR_TOKEN
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Sharing & Collaboration
|
|
180
|
+
|
|
181
|
+
Share the URL (including the token) with collaborators:
|
|
106
182
|
|
|
107
|
-
Just share the URL with the token:
|
|
108
183
|
```
|
|
109
184
|
https://your-server.com?token=abc123xyz
|
|
110
185
|
```
|
|
111
186
|
|
|
112
|
-
|
|
113
|
-
- Real-time collaborative editing
|
|
114
|
-
- Code execution
|
|
115
|
-
-
|
|
187
|
+
Everyone with the URL gets:
|
|
188
|
+
- Real-time collaborative editing
|
|
189
|
+
- Code execution on your server
|
|
190
|
+
- Full MRMD features
|
|
191
|
+
|
|
192
|
+
---
|
|
116
193
|
|
|
117
194
|
## Architecture
|
|
118
195
|
|
|
119
|
-
|
|
196
|
+
MRMD Server mirrors the Electron app's IPC interface as an HTTP API:
|
|
120
197
|
|
|
121
|
-
| Electron (IPC) |
|
|
198
|
+
| Electron (IPC) | MRMD Server (HTTP) |
|
|
122
199
|
|----------------|-------------------|
|
|
123
200
|
| `electronAPI.project.get(path)` | `GET /api/project?path=...` |
|
|
124
201
|
| `electronAPI.file.write(path, content)` | `POST /api/file/write` |
|
|
125
202
|
| `electronAPI.session.forDocument(path)` | `POST /api/session/for-document` |
|
|
126
203
|
| `ipcRenderer.on('project:changed', cb)` | WebSocket `/events` |
|
|
127
204
|
|
|
128
|
-
The browser loads
|
|
205
|
+
The browser loads an HTTP shim that creates `window.electronAPI` making HTTP calls instead of IPC. The UI code works unchanged.
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
209
|
+
│ mrmd-server │
|
|
210
|
+
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
211
|
+
│ │ Express │ │ mrmd-sync │ │ mrmd- │ │
|
|
212
|
+
│ │ HTTP API │ │ (Yjs) │ │ python/bash │ │
|
|
213
|
+
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
214
|
+
└─────────────────────────────────────────────────────────────┘
|
|
215
|
+
│ │ │
|
|
216
|
+
HTTP/REST WebSocket Execution
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
129
220
|
|
|
130
221
|
## API Reference
|
|
131
222
|
|
|
132
223
|
### Authentication
|
|
133
224
|
|
|
134
|
-
All
|
|
135
|
-
|
|
136
|
-
Provide token via:
|
|
225
|
+
All `/api/*` endpoints require authentication. Provide token via:
|
|
137
226
|
- Query parameter: `?token=xxx`
|
|
138
227
|
- Header: `Authorization: Bearer xxx`
|
|
139
228
|
- Header: `X-Token: xxx`
|
|
140
229
|
|
|
141
|
-
### Endpoints
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
- `POST /api/project/watch` - Watch for changes
|
|
154
|
-
- `POST /api/project/unwatch` - Stop watching
|
|
155
|
-
|
|
156
|
-
#### Session
|
|
157
|
-
- `GET /api/session` - List sessions
|
|
158
|
-
- `POST /api/session` - Start session
|
|
159
|
-
- `DELETE /api/session/:name` - Stop session
|
|
160
|
-
- `POST /api/session/for-document` - Get/create session for document
|
|
161
|
-
|
|
162
|
-
#### Bash
|
|
163
|
-
- Same as Session, at `/api/bash/*`
|
|
164
|
-
|
|
165
|
-
#### File
|
|
166
|
-
- `GET /api/file/scan` - Scan for files
|
|
167
|
-
- `POST /api/file/create` - Create file
|
|
168
|
-
- `POST /api/file/create-in-project` - Create with FSML ordering
|
|
169
|
-
- `POST /api/file/move` - Move/rename
|
|
170
|
-
- `POST /api/file/reorder` - Drag-drop reorder
|
|
171
|
-
- `DELETE /api/file?path=...` - Delete file
|
|
172
|
-
- `GET /api/file/read?path=...` - Read file
|
|
173
|
-
- `POST /api/file/write` - Write file
|
|
174
|
-
|
|
175
|
-
#### Asset
|
|
176
|
-
- `GET /api/asset` - List assets
|
|
177
|
-
- `POST /api/asset/save` - Upload asset
|
|
178
|
-
- `GET /api/asset/relative-path` - Calculate relative path
|
|
179
|
-
- `GET /api/asset/orphans` - Find orphaned assets
|
|
180
|
-
- `DELETE /api/asset` - Delete asset
|
|
181
|
-
|
|
182
|
-
#### Runtime
|
|
183
|
-
- `GET /api/runtime` - List runtimes
|
|
184
|
-
- `DELETE /api/runtime/:id` - Kill runtime
|
|
185
|
-
- `POST /api/runtime/:id/attach` - Attach to runtime
|
|
230
|
+
### Core Endpoints
|
|
231
|
+
|
|
232
|
+
| Endpoint | Description |
|
|
233
|
+
|----------|-------------|
|
|
234
|
+
| `GET /health` | Health check (no auth) |
|
|
235
|
+
| `GET /auth/validate?token=xxx` | Validate token (no auth) |
|
|
236
|
+
| `GET /api/project?path=...` | Get project info |
|
|
237
|
+
| `GET /api/file/read?path=...` | Read file |
|
|
238
|
+
| `POST /api/file/write` | Write file |
|
|
239
|
+
| `POST /api/session/for-document` | Get/create session for document |
|
|
240
|
+
| `GET /api/runtime` | List active runtimes |
|
|
241
|
+
| `DELETE /api/runtime/:id` | Kill a runtime |
|
|
186
242
|
|
|
187
243
|
### WebSocket Events
|
|
188
244
|
|
|
189
|
-
Connect to `/events?token=xxx`
|
|
245
|
+
Connect to `/events?token=xxx` for real-time updates:
|
|
190
246
|
|
|
191
247
|
```javascript
|
|
192
248
|
const ws = new WebSocket('wss://server.com/events?token=xxx');
|
|
193
249
|
ws.onmessage = (e) => {
|
|
194
250
|
const { event, data } = JSON.parse(e.data);
|
|
195
|
-
//
|
|
251
|
+
// Events: 'project:changed', 'venv-found', 'sync-server-died', etc.
|
|
196
252
|
};
|
|
197
253
|
```
|
|
198
254
|
|
|
199
|
-
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Security
|
|
258
|
+
|
|
259
|
+
1. **Always use HTTPS** in production — use nginx, caddy, or a cloud load balancer
|
|
260
|
+
2. **Keep tokens secret** — treat them like passwords
|
|
261
|
+
3. **Never use `--no-auth` on public networks**
|
|
262
|
+
4. **Rotate tokens** if you suspect they're compromised
|
|
200
263
|
|
|
201
|
-
|
|
202
|
-
2. **Keep tokens secret** - treat them like passwords
|
|
203
|
-
3. **Use `--no-auth` only for local development**
|
|
204
|
-
4. **Rotate tokens** if compromised
|
|
264
|
+
---
|
|
205
265
|
|
|
206
266
|
## Limitations
|
|
207
267
|
|
|
208
|
-
Some
|
|
268
|
+
Some desktop features don't work in browser mode:
|
|
209
269
|
|
|
210
270
|
| Feature | Browser Behavior |
|
|
211
271
|
|---------|------------------|
|
|
212
|
-
|
|
|
213
|
-
|
|
|
214
|
-
|
|
|
215
|
-
|
|
272
|
+
| "Show in Finder" | Returns path (can't open native file browser) |
|
|
273
|
+
| Native window controls | Standard browser chrome |
|
|
274
|
+
| Offline mode | Requires server connection |
|
|
275
|
+
|
|
276
|
+
---
|
|
216
277
|
|
|
217
278
|
## Development
|
|
218
279
|
|
|
219
280
|
```bash
|
|
281
|
+
# Clone the monorepo
|
|
282
|
+
git clone https://github.com/MaximeRivest/mrmd-packages.git
|
|
283
|
+
cd mrmd-packages/mrmd-server
|
|
284
|
+
|
|
285
|
+
# Install dependencies
|
|
286
|
+
npm install
|
|
287
|
+
|
|
220
288
|
# Run in dev mode
|
|
221
289
|
npm run dev
|
|
222
|
-
|
|
223
|
-
# The server will:
|
|
224
|
-
# - Watch for file changes
|
|
225
|
-
# - Auto-restart on changes
|
|
226
290
|
```
|
|
227
291
|
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Related Projects
|
|
295
|
+
|
|
296
|
+
| Project | Description |
|
|
297
|
+
|---------|-------------|
|
|
298
|
+
| [MRMD Electron](https://github.com/MaximeRivest/mrmd-electron) | Desktop app (macOS, Windows, Linux) |
|
|
299
|
+
| [mrmd-python](https://github.com/MaximeRivest/mrmd-python) | Python execution runtime |
|
|
300
|
+
| [mrmd-editor](https://github.com/MaximeRivest/mrmd-editor) | CodeMirror-based editor component |
|
|
301
|
+
| [mrmd-sync](https://github.com/MaximeRivest/mrmd-sync) | Yjs collaboration server |
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
228
305
|
## License
|
|
229
306
|
|
|
230
|
-
MIT
|
|
307
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
<p align="center">
|
|
312
|
+
<b>MRMD Server</b> — Your notebooks, anywhere.
|
|
313
|
+
</p>
|
package/bin/cli.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mrmd-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
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",
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"multer": "^1.4.5-lts.1",
|
|
46
46
|
"chokidar": "^3.6.0",
|
|
47
47
|
"fzf": "^0.5.2",
|
|
48
|
-
"mrmd-project": "^0.1.
|
|
48
|
+
"mrmd-project": "^0.1.2",
|
|
49
49
|
"mrmd-electron": "^0.3.4",
|
|
50
|
-
"mrmd-sync": "^0.3.
|
|
50
|
+
"mrmd-sync": "^0.3.3"
|
|
51
51
|
}
|
|
52
52
|
}
|
package/src/api/file.js
CHANGED
|
@@ -27,8 +27,8 @@ export function createFileRoutes(ctx) {
|
|
|
27
27
|
const os = await import('os');
|
|
28
28
|
const root = req.query.root || ctx.projectDir || process.cwd() || os.default.homedir();
|
|
29
29
|
const options = {
|
|
30
|
-
// Default to
|
|
31
|
-
extensions: req.query.extensions?.split(',') || ['.md', '.ipynb'],
|
|
30
|
+
// Default to markdown-like docs and .ipynb (like Electron)
|
|
31
|
+
extensions: req.query.extensions?.split(',') || ['.md', '.qmd', '.ipynb'],
|
|
32
32
|
maxDepth: parseInt(req.query.maxDepth) || 10,
|
|
33
33
|
includeHidden: req.query.includeHidden === 'true',
|
|
34
34
|
};
|
|
@@ -58,10 +58,10 @@ export function createFileRoutes(ctx) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
const fullPath = resolvePath(ctx.projectDir, filePath);
|
|
61
|
-
|
|
61
|
+
await fileService.createFile(fullPath, content);
|
|
62
62
|
|
|
63
63
|
ctx.eventBus.projectChanged(ctx.projectDir);
|
|
64
|
-
res.json({ success: true, path:
|
|
64
|
+
res.json({ success: true, path: fullPath });
|
|
65
65
|
} catch (err) {
|
|
66
66
|
if (err.message?.includes('already exists')) {
|
|
67
67
|
return res.status(409).json({ error: 'File already exists' });
|
|
@@ -89,7 +89,7 @@ export function createFileRoutes(ctx) {
|
|
|
89
89
|
ctx.eventBus.projectChanged(root);
|
|
90
90
|
res.json({
|
|
91
91
|
success: true,
|
|
92
|
-
path: result
|
|
92
|
+
path: result,
|
|
93
93
|
});
|
|
94
94
|
} catch (err) {
|
|
95
95
|
console.error('[file:createInProject]', err);
|
|
@@ -239,7 +239,7 @@ export function createFileRoutes(ctx) {
|
|
|
239
239
|
const content = await fileService.read(fullPath);
|
|
240
240
|
const previewLines = content.split('\n').slice(0, lines).join('\n');
|
|
241
241
|
|
|
242
|
-
res.json({ success: true, content: previewLines });
|
|
242
|
+
res.json({ success: true, content: previewLines, preview: previewLines });
|
|
243
243
|
} catch (err) {
|
|
244
244
|
if (err.code === 'ENOENT') {
|
|
245
245
|
return res.status(404).json({ success: false, error: 'File not found' });
|