mdshelf 1.1.0 → 1.1.2

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 CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  Turn any folder of Markdown files into a fast, beautiful, browsable site. Point mdshelf at your OpenClaw, Hermes, GBrain, wikis, notes, docs, etc. — mount each one under its own URL (e.g. `/agent`, `/hermes`, `gbrain`, `/docs`, `/notes`, `/blog`) — and read them anywhere with hot reload, clean typography, and a mobile-first theme out of the box.
4
4
 
5
- Pairs beautifully with [Tailscale](https://tailscale.com): one command serves your shelf to every device on your tailnet — phone, iPad, laptop. Sharing with Tailscale is fully private by default: no port forwarding, no public DNS, nothing exposed to the open internet.
5
+ Pairs beautifully with [Tailscale](https://tailscale.com): one command serves your shelf to every device on your tailnet — phone, iPad, laptop. Serving over Tailscale is fully private by default: no port forwarding, no public DNS, nothing exposed to the open internet.
6
+
7
+ Putting a shelf on the open internet? `mdshelf serve --auth google` signs readers in with Google and shows each one only the pages their address is allowed to read — the rules live in each file's frontmatter. Signed-in readers can hand anyone a link to a single page that expires on its own and can be revoked at any time.
6
8
 
7
9
  One command installs it as a native system service (launchd, systemd, or Windows SCM) that runs quietly in the background and survives reboots.
8
10
 
@@ -25,6 +27,7 @@ Ships as a single static Rust binary with zero runtime dependencies.
25
27
  - Auto-generated index pages for folders that have no `index.md`
26
28
  - Home page listing all mounted sites with page counts
27
29
  - Copy or download any page's Markdown source straight from its header
30
+ - **Search** — keyword and semantic search across every mounted site, with [qmd](https://github.com/tobi/qmd) ranking and results limited to pages the reader may open
28
31
 
29
32
  **Theme**
30
33
  - Bundled `mdshelf-theme` — mobile-first, responsive, light/dark aware
@@ -33,6 +36,13 @@ Ships as a single static Rust binary with zero runtime dependencies.
33
36
  - Per-site accent colors — auto-assigned from a palette or configured explicitly
34
37
  - MiniJinja templates for layouts and partials — override any file without forking the whole theme
35
38
 
39
+ **Access**
40
+ - **Google sign-in** — `--auth google` puts every page behind sign-in with your own OAuth client
41
+ - Per-page access rules in frontmatter (`allow` / `deny`), inherited through folder `index.md` files; anything not granted is denied
42
+ - **Share links** — one page, no sign-in, expiring and revocable, from a Share button or `mdshelf share`
43
+ - Access log with `mdshelf audit`; rule debugging with `mdshelf acl explain` and `mdshelf acl doctor`
44
+ - Built-in HTTPS: Let's Encrypt, your own certificate, or TLS terminated by a proxy in front
45
+
36
46
  **Server**
37
47
  - **Hot reload** — WebSocket-based live reload; the browser refreshes automatically the moment you save a file
38
48
  - Response compression (gzip / brotli / zstd via tower-http)
@@ -113,7 +123,7 @@ Open `http://127.0.0.1:4444/` for the site index.
113
123
  | Command | Description |
114
124
  |---|---|
115
125
  | `mdshelf init [dir]` | Scaffold config and sample content |
116
- | `mdshelf serve` | Start the web server |
126
+ | `mdshelf serve` | Start the web server (`--auth google` for a private site) |
117
127
  | `mdshelf check` | Validate config and scan content |
118
128
  | `mdshelf export` | Export a static bundle of HTML and CSS files |
119
129
  | `mdshelf auth setup` | Walk through creating a Google OAuth client, then verify it |
@@ -124,7 +134,7 @@ Open `http://127.0.0.1:4444/` for the site index.
124
134
  | `mdshelf share <path>` | Create a link that shares one page with anyone holding the URL |
125
135
  | `mdshelf share list` | List share links (`--all` for dead ones, `--json` for an inventory) |
126
136
  | `mdshelf share revoke <id>` | Revoke one link, or `--all` to revoke every live link |
127
- | `mdshelf install` | Register as a native system service |
137
+ | `mdshelf install` | Register `serve`, with any of its options, as a native system service |
128
138
  | `mdshelf start` | Start the installed service |
129
139
  | `mdshelf stop` | Stop the installed service |
130
140
  | `mdshelf restart` | Restart the installed service |
@@ -300,7 +310,7 @@ mid-page. Sessions expire absolutely after `session_max_age` (default 30 days).
300
310
  > qualification. Each such event is logged at WARN with the cause, so an outage is
301
311
  > visible in the logs rather than presenting as mysterious sign-outs.
302
312
 
303
- ### Static export
313
+ ### Exporting a private vault
304
314
 
305
315
  A static bundle has no authentication, so exporting a vault that declares rules
306
316
  requires saying whose view it is:
@@ -430,12 +440,69 @@ Keep `mdshelf share list --json` if you want an inventory of what a deletion wou
430
440
  before creating a folder's `index.md`. Serving, checking, exporting, and explaining are
431
441
  strictly read-only.
432
442
 
443
+ ## Search
444
+
445
+ With a `[search]` section, every page gets a search field in its header (`/` or
446
+ <kbd>Cmd</kbd>/<kbd>Ctrl</kbd>+<kbd>K</kbd> opens it). Results update as you type, with the
447
+ matching words highlighted; <kbd>Enter</kbd> opens the selected page, and **All results**
448
+ opens the full results page at `/__search?q=…`. It finds pages both by the words they
449
+ contain and by meaning — "how do we handle incidents" finds an incident response page
450
+ that never uses those words.
451
+
452
+ mdshelf does not rank pages itself. It works with a [qmd](https://github.com/tobi/qmd)
453
+ server (`npm install -g @tobilu/qmd`), and splits the work so that access rules stay
454
+ mdshelf's:
455
+
456
+ 1. **mdshelf writes a corpus.** At startup and after every content change, mdshelf writes
457
+ each published page's body — frontmatter removed, drafts left out — to
458
+ `<corpus>/<collection>/<page path>`. The collection is the site's mount without its
459
+ leading `/` (`/docs/v2` → `docs-v2`). A page whose file does not end in `.md` gets `.md`
460
+ appended, because qmd indexes `*.md`.
461
+ 2. **qmd indexes the corpus** and answers queries over HTTP. It never sees the vault, so
462
+ `allow` and `deny` lists are not searchable text.
463
+ 3. **mdshelf filters what qmd ranks.** Each ranked page is looked up exactly as a page
464
+ request would be, for the reader asking. Pages they may not open and drafts are left
465
+ out, so a query that matches only restricted pages answers exactly like one that
466
+ matches nothing. Titles and snippets come from mdshelf's own copy of the page.
467
+
468
+ ```toml
469
+ [search]
470
+ qmd_url = "http://127.0.0.1:4600" # the qmd server holding this site's index
471
+ corpus = "/var/lib/mdshelf/search" # a directory of its own: mdshelf deletes stale files here
472
+ ```
473
+
474
+ Set up qmd once, with one collection per site, pointing at the corpus:
475
+
476
+ ```bash
477
+ qmd --index mysite collection add /var/lib/mdshelf/search/docs --name docs
478
+ qmd --index mysite embed # downloads the ~300 MB embedding model once
479
+ qmd --index mysite mcp --http --host 127.0.0.1 --port 4600
480
+ ```
481
+
482
+ Keep the index current by running `qmd --index mysite update && qmd --index mysite embed`
483
+ on a timer; both only process what changed. Restart the qmd server after adding a
484
+ collection, since it reads the collection list at startup.
485
+
486
+ mdshelf asks qmd for keyword and vector matches fused by rank, without qmd's language-model
487
+ reranker, so only the embedding model is loaded: about 800 MB of memory per qmd server,
488
+ with queries in tens of milliseconds once warm. The first query after qmd starts loads
489
+ the model and takes several seconds. If qmd is unreachable, the results page says search
490
+ is unavailable rather than showing no matches.
491
+
492
+ Notes:
493
+
494
+ - Run qmd with `CI` unset: qmd refuses to load models when `CI` is set.
495
+ - qmd always skips folders named `node_modules`, `.git`, `.cache`, `vendor`, `dist` and
496
+ `build`, so pages under such folders are not searchable.
497
+ - Share-link readers get no search; anonymous visitors get the sign-in page, as for any
498
+ other path.
499
+
433
500
  ## Theme customisation
434
501
 
435
502
  Override or extend templates by pointing `[theme].directory` at a local directory:
436
503
 
437
- - `layouts/` — `base.html`, `doc.html`, `home.html`, `error.html`, `link.html`
438
- - `partials/` — `header.html`, `sidebar.html`, `toc.html`, `breadcrumbs.html`, `footer.html`
504
+ - `layouts/` — `base.html`, `doc.html`, `home.html`, `error.html`, `link.html`, `search.html`
505
+ - `partials/` — `header.html`, `sidebar.html`, `toc.html`, `breadcrumbs.html`, `footer.html`, `search.html`
439
506
  - `assets/` — static files served from `/__assets/...`
440
507
 
441
508
  ### Theme capabilities
@@ -468,6 +535,35 @@ sudo mdshelf uninstall
468
535
 
469
536
  Use `--user` for a per-user service where the platform supports it.
470
537
 
538
+ `mdshelf install` takes every `serve` option and the service runs with exactly those, so a
539
+ private site installs the same way it serves:
540
+
541
+ ```bash
542
+ sudo mdshelf auth setup # store Google credentials for the user installing the service
543
+ sudo mdshelf install --auth google --behind-proxy --public-url https://docs.acme.com
544
+ sudo mdshelf start
545
+ ```
546
+
547
+ Install runs the checks `serve` runs before it binds — TLS flags, the `[auth]` and `[links]`
548
+ settings, and the stored credentials — and refuses to register a service that could not
549
+ start. It prints the redirect URI to register with Google.
550
+
551
+ - **Credentials come from the file, not your shell.** A service does not inherit the
552
+ installing shell's `MDSHELF_GOOGLE_*` variables, so install requires both in
553
+ `~/.config/mdshelf/credentials.env` (what `mdshelf auth setup` writes). Run `auth setup`
554
+ as the same user that runs `install` — with `sudo`, that is root.
555
+ - **The service gets the installer's `HOME`.** With `--auth` or `--domain`, the service is
556
+ given the installing user's `HOME`, so the credentials, the at-rest key
557
+ (`~/.config/mdshelf/secret.key`) and the ACME cache resolve to the paths install checked.
558
+ - **Relative paths are pinned** to the directory you ran install in: `--tls-cert`,
559
+ `--tls-key` and `--acme-cache` are stored as absolute paths.
560
+ - **Paths with spaces** work with launchd, systemd and Windows: install quotes each
561
+ argument for systemd, so `%` and `$` stay literal too.
562
+ - **OpenRC, rc.d and Windows** services are not given environment variables, so `install`
563
+ refuses `--auth`, and `--domain` without `--acme-cache`, there. OpenRC and rc.d also
564
+ split their command on spaces with no way to quote, so install refuses paths or values
565
+ containing spaces or shell characters on those systems.
566
+
471
567
  ## Serving on port 80
472
568
 
473
569
  Set `host` and `port` in your config:
@@ -504,7 +600,7 @@ Tailscale provisions a certificate automatically and makes the site available **
504
600
  tailscale serve status # confirm the URL
505
601
  ```
506
602
 
507
- Pair it with `sudo mdshelf install && sudo mdshelf start` (see [System service](#system-service)) so both services survive reboots.
603
+ Pair it with `sudo mdshelf install && sudo mdshelf start` (see [System service](#system-service)) so both services survive reboots; pass the same `--auth` and TLS flags to `install` as to `serve`.
508
604
 
509
605
  To stop serving:
510
606
 
@@ -514,7 +610,16 @@ tailscale serve off
514
610
 
515
611
  ### Public access via Tailscale Funnel
516
612
 
517
- To make the site reachable from the **public internet** (not just your tailnet), use Funnel instead:
613
+ To make the site reachable from the **public internet** (not just your tailnet), use Funnel instead.
614
+ Funnel publishes everything mdshelf serves to anyone, so serve a private site with sign-in on
615
+ and Tailscale's hostname as the public URL:
616
+
617
+ ```bash
618
+ mdshelf serve --auth google --behind-proxy --public-url https://<machine>.<tailnet>.ts.net
619
+ ```
620
+
621
+ Register `https://<machine>.<tailnet>.ts.net/auth/callback` as a redirect URI on your Google
622
+ OAuth client, then:
518
623
 
519
624
  ```bash
520
625
  tailscale funnel --bg 4444
@@ -556,7 +661,7 @@ iPadOS Safari mishandle that, so there they go through `GET /__mdshelf/md/<page>
556
661
  enforces exactly the same access rules as viewing the page — a page you may not read
557
662
  returns the same response as one that does not exist.
558
663
 
559
- ### Static export
664
+ ### Exporting a static site
560
665
 
561
666
  ```bash
562
667
  mdshelf export # all sites → ./dist
@@ -23,7 +23,7 @@
23
23
  "hasInstallScript": true,
24
24
  "license": "MIT OR Apache-2.0",
25
25
  "name": "mdshelf",
26
- "version": "1.1.0"
26
+ "version": "1.1.2"
27
27
  },
28
28
  "node_modules/@isaacs/cliui": {
29
29
  "engines": {
@@ -542,5 +542,5 @@
542
542
  }
543
543
  },
544
544
  "requires": true,
545
- "version": "1.1.0"
545
+ "version": "1.1.2"
546
546
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "artifactDownloadUrls": [
3
- "https://github.com/prbski/mdshelf/releases/download/v1.1.0"
3
+ "https://github.com/prbski/mdshelf/releases/download/v1.1.2"
4
4
  ],
5
5
  "author": "Patryk Porebski",
6
6
  "bin": {
@@ -100,7 +100,7 @@
100
100
  "zipExt": ".tar.xz"
101
101
  }
102
102
  },
103
- "version": "1.1.0",
103
+ "version": "1.1.2",
104
104
  "volta": {
105
105
  "node": "18.14.1",
106
106
  "npm": "9.5.0"