mrmd-server 0.1.0

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 ADDED
@@ -0,0 +1,230 @@
1
+ # mrmd-server
2
+
3
+ Run mrmd in any browser. Access your notebooks from anywhere.
4
+
5
+ ```
6
+ ┌─────────────────────────────────────────────────────────────┐
7
+ │ Your VPS / Cloud Server │
8
+ │ │
9
+ │ ┌─────────────────────────────────────────────────────┐ │
10
+ │ │ mrmd-server │ │
11
+ │ │ • HTTP API (full electronAPI equivalent) │ │
12
+ │ │ • Static file serving │ │
13
+ │ │ • WebSocket for real-time events │ │
14
+ │ │ • Token authentication │ │
15
+ │ └─────────────────────────────────────────────────────┘ │
16
+ │ │
17
+ └─────────────────────────────────────────────────────────────┘
18
+
19
+ │ https://your-server.com?token=xxx
20
+
21
+ ┌───────────┴───────────┬─────────────────┐
22
+ │ │ │
23
+ ┌───▼───┐ ┌────▼────┐ ┌────▼────┐
24
+ │Laptop │ │ Phone │ │ Collab │
25
+ └───────┘ └─────────┘ └─────────┘
26
+ ```
27
+
28
+ ## Features
29
+
30
+ - **Same UI as Electron** - Uses the exact same index.html from mrmd-electron
31
+ - **Access from anywhere** - Phone, tablet, any browser
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
35
+
36
+ ## Quick Start
37
+
38
+ ```bash
39
+ # Install
40
+ cd mrmd-packages/mrmd-server
41
+ npm install
42
+
43
+ # Start server in your project directory
44
+ npx mrmd-server ./my-notebooks
45
+
46
+ # Output:
47
+ # mrmd-server
48
+ # ──────────────────────────────────────────────────────
49
+ # Server: http://0.0.0.0:8080
50
+ # Project: /home/you/my-notebooks
51
+ # Token: abc123xyz...
52
+ #
53
+ # Access URL:
54
+ # http://localhost:8080?token=abc123xyz...
55
+ ```
56
+
57
+ ## Usage
58
+
59
+ ### Basic Usage
60
+
61
+ ```bash
62
+ # Start in current directory
63
+ mrmd-server
64
+
65
+ # Start in specific directory
66
+ mrmd-server ./my-project
67
+
68
+ # Custom port
69
+ mrmd-server -p 3000 ./my-project
70
+
71
+ # With specific token
72
+ mrmd-server -t my-secret-token ./my-project
73
+
74
+ # No auth (local development only!)
75
+ mrmd-server --no-auth ./my-project
76
+ ```
77
+
78
+ ### Remote Access
79
+
80
+ 1. Start mrmd-server on your VPS:
81
+ ```bash
82
+ mrmd-server -p 8080 /home/you/notebooks
83
+ ```
84
+
85
+ 2. Set up HTTPS (recommended) with nginx or caddy:
86
+ ```nginx
87
+ server {
88
+ listen 443 ssl;
89
+ server_name notebooks.example.com;
90
+
91
+ location / {
92
+ proxy_pass http://localhost:8080;
93
+ proxy_http_version 1.1;
94
+ proxy_set_header Upgrade $http_upgrade;
95
+ proxy_set_header Connection "upgrade";
96
+ }
97
+ }
98
+ ```
99
+
100
+ 3. Access from anywhere:
101
+ ```
102
+ https://notebooks.example.com?token=YOUR_TOKEN
103
+ ```
104
+
105
+ ### Share with Collaborators
106
+
107
+ Just share the URL with the token:
108
+ ```
109
+ https://your-server.com?token=abc123xyz
110
+ ```
111
+
112
+ Collaborators get:
113
+ - Real-time collaborative editing (Yjs)
114
+ - Code execution (via the server)
115
+ - Same UI as local Electron app
116
+
117
+ ## Architecture
118
+
119
+ mrmd-server provides an HTTP API that mirrors Electron's IPC interface:
120
+
121
+ | Electron (IPC) | mrmd-server (HTTP) |
122
+ |----------------|-------------------|
123
+ | `electronAPI.project.get(path)` | `GET /api/project?path=...` |
124
+ | `electronAPI.file.write(path, content)` | `POST /api/file/write` |
125
+ | `electronAPI.session.forDocument(path)` | `POST /api/session/for-document` |
126
+ | `ipcRenderer.on('project:changed', cb)` | WebSocket `/events` |
127
+
128
+ The browser loads `http-shim.js` which creates a `window.electronAPI` object that makes HTTP calls instead of IPC calls. The existing UI code works unchanged.
129
+
130
+ ## API Reference
131
+
132
+ ### Authentication
133
+
134
+ All API endpoints (except `/health` and `/auth/validate`) require authentication.
135
+
136
+ Provide token via:
137
+ - Query parameter: `?token=xxx`
138
+ - Header: `Authorization: Bearer xxx`
139
+ - Header: `X-Token: xxx`
140
+
141
+ ### Endpoints
142
+
143
+ #### System
144
+ - `GET /api/system/home` - Get home directory
145
+ - `GET /api/system/recent` - Get recent files/venvs
146
+ - `GET /api/system/ai` - Get AI server info
147
+ - `POST /api/system/discover-venvs` - Start venv discovery
148
+
149
+ #### Project
150
+ - `GET /api/project?path=...` - Get project info
151
+ - `POST /api/project` - Create project
152
+ - `GET /api/project/nav?root=...` - Get navigation tree
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
186
+
187
+ ### WebSocket Events
188
+
189
+ Connect to `/events?token=xxx` to receive push events:
190
+
191
+ ```javascript
192
+ const ws = new WebSocket('wss://server.com/events?token=xxx');
193
+ ws.onmessage = (e) => {
194
+ const { event, data } = JSON.parse(e.data);
195
+ // event: 'project:changed', 'venv-found', 'sync-server-died', etc.
196
+ };
197
+ ```
198
+
199
+ ## Security Considerations
200
+
201
+ 1. **Always use HTTPS** in production (use nginx/caddy as reverse proxy)
202
+ 2. **Keep tokens secret** - treat them like passwords
203
+ 3. **Use `--no-auth` only for local development**
204
+ 4. **Rotate tokens** if compromised
205
+
206
+ ## Limitations
207
+
208
+ Some Electron features can't work in browser:
209
+
210
+ | Feature | Browser Behavior |
211
+ |---------|------------------|
212
+ | `shell.showItemInFolder` | Returns path (can't open Finder) |
213
+ | `shell.openPath` | Returns path (can't open local apps) |
214
+ | Native titlebar | Standard browser chrome |
215
+ | Offline | Requires server connection |
216
+
217
+ ## Development
218
+
219
+ ```bash
220
+ # Run in dev mode
221
+ npm run dev
222
+
223
+ # The server will:
224
+ # - Watch for file changes
225
+ # - Auto-restart on changes
226
+ ```
227
+
228
+ ## License
229
+
230
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * mrmd-server CLI
5
+ *
6
+ * Usage:
7
+ * mrmd-server [options] [project-dir]
8
+ *
9
+ * Options:
10
+ * -p, --port <port> HTTP port (default: 8080)
11
+ * -h, --host <host> Bind host (default: 0.0.0.0)
12
+ * -t, --token <token> Auth token (auto-generated if not provided)
13
+ * --no-auth Disable authentication (dangerous!)
14
+ * --help Show help
15
+ */
16
+
17
+ import { createServer } from '../src/server.js';
18
+ import { spawn } from 'child_process';
19
+ import path from 'path';
20
+ import fs from 'fs/promises';
21
+
22
+ function parseArgs(args) {
23
+ const options = {
24
+ port: 8080,
25
+ host: '0.0.0.0',
26
+ token: null,
27
+ noAuth: false,
28
+ projectDir: '.',
29
+ };
30
+
31
+ for (let i = 0; i < args.length; i++) {
32
+ const arg = args[i];
33
+
34
+ if (arg === '-p' || arg === '--port') {
35
+ options.port = parseInt(args[++i], 10);
36
+ } else if (arg === '-h' || arg === '--host') {
37
+ options.host = args[++i];
38
+ } else if (arg === '-t' || arg === '--token') {
39
+ options.token = args[++i];
40
+ } else if (arg === '--no-auth') {
41
+ options.noAuth = true;
42
+ } else if (arg === '--help') {
43
+ printHelp();
44
+ process.exit(0);
45
+ } else if (!arg.startsWith('-')) {
46
+ options.projectDir = arg;
47
+ }
48
+ }
49
+
50
+ return options;
51
+ }
52
+
53
+ function printHelp() {
54
+ console.log(`
55
+ mrmd-server - Run mrmd in any browser
56
+
57
+ Usage:
58
+ mrmd-server [options] [project-dir]
59
+
60
+ Options:
61
+ -p, --port <port> HTTP port (default: 8080)
62
+ -h, --host <host> Bind host (default: 0.0.0.0)
63
+ -t, --token <token> Auth token (auto-generated if not provided)
64
+ --no-auth Disable authentication (DANGEROUS - local dev only)
65
+ --help Show this help
66
+
67
+ Examples:
68
+ mrmd-server Start in current directory
69
+ mrmd-server ./my-project Start in specific directory
70
+ mrmd-server -p 3000 ./notebooks Custom port
71
+ mrmd-server --no-auth No auth (local dev only)
72
+
73
+ Access:
74
+ Once started, access via the URL shown (includes token).
75
+ Share the URL with collaborators for real-time editing.
76
+ `);
77
+ }
78
+
79
+ async function main() {
80
+ const args = process.argv.slice(2);
81
+ const options = parseArgs(args);
82
+
83
+ // Resolve project directory
84
+ options.projectDir = path.resolve(options.projectDir);
85
+
86
+ // Verify directory exists
87
+ try {
88
+ await fs.access(options.projectDir);
89
+ } catch {
90
+ console.error(`Error: Directory not found: ${options.projectDir}`);
91
+ process.exit(1);
92
+ }
93
+
94
+ // Find mrmd-electron for the UI
95
+ const packageDir = path.dirname(path.dirname(import.meta.url.replace('file://', '')));
96
+ const possibleElectronPaths = [
97
+ path.join(packageDir, '..', 'mrmd-electron'),
98
+ path.join(process.cwd(), '..', 'mrmd-electron'),
99
+ path.join(process.cwd(), 'mrmd-electron'),
100
+ ];
101
+
102
+ let electronDir = null;
103
+ for (const p of possibleElectronPaths) {
104
+ try {
105
+ await fs.access(path.join(p, 'index.html'));
106
+ electronDir = p;
107
+ break;
108
+ } catch {}
109
+ }
110
+
111
+ // Create and start server
112
+ const server = await createServer({
113
+ ...options,
114
+ electronDir,
115
+ });
116
+
117
+ // Handle shutdown
118
+ process.on('SIGINT', async () => {
119
+ console.log('\nShutting down...');
120
+ await server.stop();
121
+ process.exit(0);
122
+ });
123
+
124
+ process.on('SIGTERM', async () => {
125
+ await server.stop();
126
+ process.exit(0);
127
+ });
128
+
129
+ await server.start();
130
+
131
+ // Start mrmd-sync if available
132
+ try {
133
+ const syncPath = path.join(packageDir, '..', 'mrmd-sync', 'bin', 'cli.js');
134
+ await fs.access(syncPath);
135
+
136
+ console.log(' Starting mrmd-sync...');
137
+ const syncProc = spawn('node', [syncPath, '--port', '4444', options.projectDir], {
138
+ stdio: ['pipe', 'pipe', 'pipe'],
139
+ });
140
+
141
+ syncProc.stdout.on('data', (data) => {
142
+ if (data.toString().includes('Server started')) {
143
+ console.log(` Sync: ws://localhost:4444`);
144
+ }
145
+ });
146
+
147
+ server.context.syncProcess = syncProc;
148
+ } catch {
149
+ console.log(' Sync: (mrmd-sync not found, start manually)');
150
+ }
151
+
152
+ // Keep running
153
+ console.log('');
154
+ console.log(' Press Ctrl+C to stop');
155
+ console.log('');
156
+ }
157
+
158
+ main().catch((err) => {
159
+ console.error('Fatal error:', err);
160
+ process.exit(1);
161
+ });
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "mrmd-server",
3
+ "version": "0.1.0",
4
+ "description": "HTTP server for mrmd - run mrmd in any browser, access from anywhere",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "mrmd-server": "./bin/cli.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node bin/cli.js",
12
+ "dev": "node bin/cli.js --dev"
13
+ },
14
+ "keywords": [
15
+ "mrmd",
16
+ "markdown",
17
+ "notebook",
18
+ "server",
19
+ "remote",
20
+ "collaboration"
21
+ ],
22
+ "license": "MIT",
23
+ "engines": {
24
+ "node": ">=18.0.0"
25
+ },
26
+ "dependencies": {
27
+ "express": "^4.18.2",
28
+ "ws": "^8.16.0",
29
+ "cors": "^2.8.5",
30
+ "multer": "^1.4.5-lts.1",
31
+ "chokidar": "^3.6.0",
32
+ "fzf": "^0.5.2",
33
+ "mrmd-project": "*"
34
+ }
35
+ }
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Asset API routes
3
+ *
4
+ * Mirrors electronAPI.asset.*
5
+ */
6
+
7
+ import { Router } from 'express';
8
+ import path from 'path';
9
+ import fs from 'fs/promises';
10
+ import crypto from 'crypto';
11
+ import multer from 'multer';
12
+
13
+ // Configure multer for file uploads
14
+ const upload = multer({
15
+ storage: multer.memoryStorage(),
16
+ limits: { fileSize: 50 * 1024 * 1024 }, // 50MB limit
17
+ });
18
+
19
+ /**
20
+ * Create asset routes
21
+ * @param {import('../server.js').ServerContext} ctx
22
+ */
23
+ export function createAssetRoutes(ctx) {
24
+ const router = Router();
25
+
26
+ /**
27
+ * GET /api/asset?projectRoot=...
28
+ * List all assets in a project
29
+ * Mirrors: electronAPI.asset.list(projectRoot)
30
+ */
31
+ router.get('/', async (req, res) => {
32
+ try {
33
+ const projectRoot = req.query.projectRoot || ctx.projectDir;
34
+ const assetsDir = path.join(projectRoot, '_assets');
35
+
36
+ try {
37
+ const files = await fs.readdir(assetsDir);
38
+ const assets = [];
39
+
40
+ for (const file of files) {
41
+ if (file.startsWith('.')) continue;
42
+
43
+ const filePath = path.join(assetsDir, file);
44
+ const stat = await fs.stat(filePath);
45
+
46
+ assets.push({
47
+ name: file,
48
+ path: `_assets/${file}`,
49
+ size: stat.size,
50
+ modified: stat.mtime.toISOString(),
51
+ });
52
+ }
53
+
54
+ res.json(assets);
55
+ } catch (err) {
56
+ if (err.code === 'ENOENT') {
57
+ return res.json([]);
58
+ }
59
+ throw err;
60
+ }
61
+ } catch (err) {
62
+ console.error('[asset:list]', err);
63
+ res.status(500).json({ error: err.message });
64
+ }
65
+ });
66
+
67
+ /**
68
+ * POST /api/asset/save
69
+ * Save an asset (handles deduplication)
70
+ * Mirrors: electronAPI.asset.save(projectRoot, file, filename)
71
+ *
72
+ * Accepts multipart form data with 'file' field
73
+ * or JSON with 'file' as base64 or array of bytes
74
+ */
75
+ router.post('/save', upload.single('file'), async (req, res) => {
76
+ try {
77
+ const projectRoot = req.body.projectRoot || ctx.projectDir;
78
+ let filename = req.body.filename || req.file?.originalname || 'untitled';
79
+ let fileBuffer;
80
+
81
+ if (req.file) {
82
+ // Multipart upload
83
+ fileBuffer = req.file.buffer;
84
+ } else if (req.body.file) {
85
+ // JSON with base64 or array
86
+ if (typeof req.body.file === 'string') {
87
+ fileBuffer = Buffer.from(req.body.file, 'base64');
88
+ } else if (Array.isArray(req.body.file)) {
89
+ fileBuffer = Buffer.from(req.body.file);
90
+ } else {
91
+ return res.status(400).json({ error: 'Invalid file format' });
92
+ }
93
+ } else {
94
+ return res.status(400).json({ error: 'No file provided' });
95
+ }
96
+
97
+ const assetsDir = path.join(projectRoot, '_assets');
98
+ await fs.mkdir(assetsDir, { recursive: true });
99
+
100
+ // Compute hash for deduplication
101
+ const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex').slice(0, 8);
102
+ const ext = path.extname(filename);
103
+ const base = path.basename(filename, ext);
104
+
105
+ // Check if identical file already exists
106
+ const existingFiles = await fs.readdir(assetsDir).catch(() => []);
107
+ for (const existing of existingFiles) {
108
+ if (existing.startsWith(hash + '-')) {
109
+ // Found duplicate
110
+ return res.json({
111
+ path: `_assets/${existing}`,
112
+ deduplicated: true,
113
+ });
114
+ }
115
+ }
116
+
117
+ // Save with hash prefix
118
+ const finalName = `${hash}-${base}${ext}`;
119
+ const finalPath = path.join(assetsDir, finalName);
120
+
121
+ await fs.writeFile(finalPath, fileBuffer);
122
+
123
+ res.json({
124
+ path: `_assets/${finalName}`,
125
+ deduplicated: false,
126
+ });
127
+ } catch (err) {
128
+ console.error('[asset:save]', err);
129
+ res.status(500).json({ error: err.message });
130
+ }
131
+ });
132
+
133
+ /**
134
+ * GET /api/asset/relative-path?assetPath=...&documentPath=...
135
+ * Get relative path from document to asset
136
+ * Mirrors: electronAPI.asset.relativePath(assetPath, documentPath)
137
+ */
138
+ router.get('/relative-path', async (req, res) => {
139
+ try {
140
+ const { assetPath, documentPath } = req.query;
141
+
142
+ if (!assetPath || !documentPath) {
143
+ return res.status(400).json({ error: 'assetPath and documentPath required' });
144
+ }
145
+
146
+ // Calculate relative path from document to asset
147
+ const docDir = path.dirname(documentPath);
148
+ const relativePath = path.relative(docDir, assetPath);
149
+
150
+ res.json({ relativePath });
151
+ } catch (err) {
152
+ console.error('[asset:relativePath]', err);
153
+ res.status(500).json({ error: err.message });
154
+ }
155
+ });
156
+
157
+ /**
158
+ * GET /api/asset/orphans?projectRoot=...
159
+ * Find orphaned assets
160
+ * Mirrors: electronAPI.asset.orphans(projectRoot)
161
+ */
162
+ router.get('/orphans', async (req, res) => {
163
+ try {
164
+ const projectRoot = req.query.projectRoot || ctx.projectDir;
165
+ const assetsDir = path.join(projectRoot, '_assets');
166
+
167
+ // Get all assets
168
+ let assetFiles;
169
+ try {
170
+ assetFiles = await fs.readdir(assetsDir);
171
+ } catch {
172
+ return res.json([]);
173
+ }
174
+
175
+ // Get all markdown files
176
+ const mdFiles = await scanMarkdownFiles(projectRoot);
177
+
178
+ // Read all markdown content and find referenced assets
179
+ const referencedAssets = new Set();
180
+ for (const mdFile of mdFiles) {
181
+ try {
182
+ const content = await fs.readFile(mdFile, 'utf-8');
183
+ // Find asset references (images, links)
184
+ const matches = content.matchAll(/!\[.*?\]\(([^)]+)\)|href="([^"]+)"/g);
185
+ for (const match of matches) {
186
+ const ref = match[1] || match[2];
187
+ if (ref && ref.includes('_assets/')) {
188
+ const assetName = path.basename(ref);
189
+ referencedAssets.add(assetName);
190
+ }
191
+ }
192
+ } catch {}
193
+ }
194
+
195
+ // Find orphans
196
+ const orphans = assetFiles.filter(f => !f.startsWith('.') && !referencedAssets.has(f));
197
+
198
+ res.json(orphans.map(f => `_assets/${f}`));
199
+ } catch (err) {
200
+ console.error('[asset:orphans]', err);
201
+ res.status(500).json({ error: err.message });
202
+ }
203
+ });
204
+
205
+ /**
206
+ * DELETE /api/asset?projectRoot=...&assetPath=...
207
+ * Delete an asset
208
+ * Mirrors: electronAPI.asset.delete(projectRoot, assetPath)
209
+ */
210
+ router.delete('/', async (req, res) => {
211
+ try {
212
+ const projectRoot = req.query.projectRoot || ctx.projectDir;
213
+ const assetPath = req.query.assetPath;
214
+
215
+ if (!assetPath) {
216
+ return res.status(400).json({ error: 'assetPath required' });
217
+ }
218
+
219
+ const fullPath = path.join(projectRoot, assetPath);
220
+
221
+ // Security check
222
+ if (!fullPath.startsWith(path.resolve(projectRoot))) {
223
+ return res.status(400).json({ error: 'Invalid path' });
224
+ }
225
+
226
+ await fs.unlink(fullPath);
227
+ res.json({ success: true });
228
+ } catch (err) {
229
+ console.error('[asset:delete]', err);
230
+ res.status(500).json({ error: err.message });
231
+ }
232
+ });
233
+
234
+ /**
235
+ * GET /api/asset/file/*
236
+ * Serve asset files (for image preview in browser)
237
+ */
238
+ router.get('/file/*', async (req, res) => {
239
+ try {
240
+ const assetPath = req.params[0];
241
+ const fullPath = path.join(ctx.projectDir, '_assets', assetPath);
242
+
243
+ // Security check
244
+ if (!fullPath.startsWith(path.resolve(ctx.projectDir))) {
245
+ return res.status(400).json({ error: 'Invalid path' });
246
+ }
247
+
248
+ res.sendFile(fullPath);
249
+ } catch (err) {
250
+ console.error('[asset:file]', err);
251
+ res.status(500).json({ error: err.message });
252
+ }
253
+ });
254
+
255
+ return router;
256
+ }
257
+
258
+ /**
259
+ * Recursively scan for markdown files
260
+ */
261
+ async function scanMarkdownFiles(dir, maxDepth = 6, currentDepth = 0) {
262
+ if (currentDepth > maxDepth) return [];
263
+
264
+ const files = [];
265
+ const entries = await fs.readdir(dir, { withFileTypes: true });
266
+
267
+ for (const entry of entries) {
268
+ if (entry.name.startsWith('.')) continue;
269
+ if (entry.name === 'node_modules') continue;
270
+ if (entry.name === '_assets') continue;
271
+
272
+ const fullPath = path.join(dir, entry.name);
273
+
274
+ if (entry.isDirectory()) {
275
+ const subFiles = await scanMarkdownFiles(fullPath, maxDepth, currentDepth + 1);
276
+ files.push(...subFiles);
277
+ } else if (entry.name.endsWith('.md')) {
278
+ files.push(fullPath);
279
+ }
280
+ }
281
+
282
+ return files;
283
+ }