@wizzlethorpe/vaults 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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -0
  3. package/dist/api.js +42 -0
  4. package/dist/api.js.map +1 -0
  5. package/dist/auth.js +62 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/build.js +758 -0
  8. package/dist/build.js.map +1 -0
  9. package/dist/commands/build.js +23 -0
  10. package/dist/commands/build.js.map +1 -0
  11. package/dist/commands/init.js +67 -0
  12. package/dist/commands/init.js.map +1 -0
  13. package/dist/commands/password.js +74 -0
  14. package/dist/commands/password.js.map +1 -0
  15. package/dist/commands/preview.js +60 -0
  16. package/dist/commands/preview.js.map +1 -0
  17. package/dist/commands/push.js +191 -0
  18. package/dist/commands/push.js.map +1 -0
  19. package/dist/commands/role.js +122 -0
  20. package/dist/commands/role.js.map +1 -0
  21. package/dist/config.js +79 -0
  22. package/dist/config.js.map +1 -0
  23. package/dist/favicon.js +91 -0
  24. package/dist/favicon.js.map +1 -0
  25. package/dist/images.js +47 -0
  26. package/dist/images.js.map +1 -0
  27. package/dist/index.js +154 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/obsidian.js +47 -0
  30. package/dist/obsidian.js.map +1 -0
  31. package/dist/render/auth-template.js +677 -0
  32. package/dist/render/auth-template.js.map +1 -0
  33. package/dist/render/callouts.js +65 -0
  34. package/dist/render/callouts.js.map +1 -0
  35. package/dist/render/embed.js +190 -0
  36. package/dist/render/embed.js.map +1 -0
  37. package/dist/render/layout.js +414 -0
  38. package/dist/render/layout.js.map +1 -0
  39. package/dist/render/mcp-template.js +239 -0
  40. package/dist/render/mcp-template.js.map +1 -0
  41. package/dist/render/pipeline.js +59 -0
  42. package/dist/render/pipeline.js.map +1 -0
  43. package/dist/render/preview.js +81 -0
  44. package/dist/render/preview.js.map +1 -0
  45. package/dist/render/slug.js +12 -0
  46. package/dist/render/slug.js.map +1 -0
  47. package/dist/render/styles.js +383 -0
  48. package/dist/render/styles.js.map +1 -0
  49. package/dist/render/types.js +2 -0
  50. package/dist/render/types.js.map +1 -0
  51. package/dist/render/wikilink.js +55 -0
  52. package/dist/render/wikilink.js.map +1 -0
  53. package/dist/scan.js +45 -0
  54. package/dist/scan.js.map +1 -0
  55. package/dist/settings.js +157 -0
  56. package/dist/settings.js.map +1 -0
  57. package/dist/util.js +60 -0
  58. package/dist/util.js.map +1 -0
  59. package/package.json +64 -0
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Standalone 404 page using the same shell as a regular article (sidebar,
3
+ * search, sitemap), so a missing page still leaves the reader inside the
4
+ * site and able to navigate out. Built once per variant.
5
+ */
6
+ export function render404(input) {
7
+ const body = `<p class="lead-404">The page you're looking for doesn't exist, or you don't have access to it.</p>
8
+ <p><a class="internal" href="/">Return to ${esc(input.vaultName)} home →</a></p>`;
9
+ return renderLayout({
10
+ ...input,
11
+ title: "Page not found",
12
+ // Neutral sentinel — breadcrumbs check this and render nothing for it.
13
+ pagePath: "__404__.md",
14
+ bodyHtml: body,
15
+ backlinks: [],
16
+ });
17
+ }
18
+ export function renderLayout(input) {
19
+ const breadcrumbs = renderBreadcrumbs(input.pagePath, input.vaultName);
20
+ const sitemap = renderSitemap(input.pages, input.pagePath);
21
+ return `<!doctype html>
22
+ <html lang="en">
23
+ <head>
24
+ <meta charset="utf-8">
25
+ <meta name="viewport" content="width=device-width,initial-scale=1">
26
+ <title>${esc(input.title)} — ${esc(input.vaultName)}</title>
27
+ <link rel="icon" href="/favicon.ico">
28
+ <link rel="stylesheet" href="/styles.css">
29
+ <link rel="stylesheet" href="/user.css">
30
+ </head>
31
+ <body${input.centerImages ? ` class="center-images"` : ""}${input.defaultImageWidth ? ` style="--default-img-width: ${attr(input.defaultImageWidth)}"` : ""}>
32
+ <div class="app-grid">
33
+ <aside class="sidebar">
34
+ <a class="brand" href="/">${esc(input.vaultName)}</a>
35
+ <div class="search-box">
36
+ <input id="vault-search" type="search" placeholder="Search…" aria-label="Search vault" autocomplete="off">
37
+ <div class="search-results" role="listbox"></div>
38
+ </div>
39
+ ${input.authConfigured ? '<div class="auth-box" id="vault-auth"></div>' : ''}
40
+ ${sitemap}
41
+ </aside>
42
+ <main>
43
+ <article class="markdown-preview-view markdown-rendered">
44
+ ${breadcrumbs}
45
+ ${input.inlineTitle ? `<h1>${esc(input.title)}</h1>` : ""}
46
+ ${renderMeta(input.mtime, input.birthtime)}
47
+ ${input.bodyHtml}
48
+ </article>
49
+ </main>
50
+ <aside class="rightbar">
51
+ <details class="toc" open>
52
+ <summary class="toc-summary">On this page</summary>
53
+ <ul id="page-toc"></ul>
54
+ </details>
55
+ ${renderBacklinks(input.backlinks)}
56
+ </aside>
57
+ </div>
58
+ ${HOVER_PREVIEW_SCRIPT}
59
+ ${TOC_SCRIPT}
60
+ ${SEARCH_SCRIPT}
61
+ ${LIGHTBOX_SCRIPT}
62
+ ${AUTH_SCRIPT}
63
+ </body>
64
+ </html>`;
65
+ }
66
+ function renderBacklinks(backlinks) {
67
+ if (backlinks.length === 0)
68
+ return "";
69
+ const items = backlinks.map((p) => {
70
+ const href = "/" + p.path.replace(/\.md$/i, "").split("/").map(encodeURIComponent).join("/");
71
+ return `<li><a href="${attr(href)}" class="internal internal-link">${esc(p.title)}</a></li>`;
72
+ }).join("");
73
+ return `<section class="backlinks"><h4>Backlinks</h4><ul>${items}</ul></section>`;
74
+ }
75
+ function renderMeta(mtime, birthtime) {
76
+ if (!mtime && !birthtime)
77
+ return "";
78
+ const parts = [];
79
+ if (birthtime && (!mtime || Math.abs(mtime - birthtime) > 60)) {
80
+ // Only show "Created" if it's meaningfully different from the modified time.
81
+ parts.push(`<span>Created <time datetime="${isoDate(birthtime)}">${formatDate(birthtime)}</time></span>`);
82
+ }
83
+ if (mtime)
84
+ parts.push(`<span>Updated <time datetime="${isoDate(mtime)}">${formatDate(mtime)}</time></span>`);
85
+ return `<div class="page-meta">${parts.join(' <span class="meta-sep">·</span> ')}</div>`;
86
+ }
87
+ function isoDate(unix) {
88
+ return new Date(unix * 1000).toISOString();
89
+ }
90
+ function formatDate(unix) {
91
+ const d = new Date(unix * 1000);
92
+ return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
93
+ }
94
+ function renderBreadcrumbs(pagePath, vaultName) {
95
+ // Sentinel used by the 404 page — no real path to crumb out of.
96
+ if (pagePath === "__404__.md")
97
+ return "";
98
+ const parts = pagePath.replace(/\.md$/i, "").split("/");
99
+ if (parts.length === 1 && parts[0] === "index")
100
+ return "";
101
+ // Folder homepages end in /index — drop that trailing segment so the crumbs
102
+ // read "Vault › DM Notes" instead of "Vault › DM Notes › index".
103
+ if (parts.length > 1 && parts[parts.length - 1] === "index")
104
+ parts.pop();
105
+ const crumbs = [`<a href="/">${esc(vaultName)}</a>`];
106
+ parts.forEach((part, i) => {
107
+ const isLast = i === parts.length - 1;
108
+ if (isLast) {
109
+ crumbs.push(`<span>${esc(part)}</span>`);
110
+ }
111
+ else {
112
+ const href = "/" + parts.slice(0, i + 1).map(encodeURIComponent).join("/");
113
+ crumbs.push(`<a href="${attr(href)}">${esc(part)}</a>`);
114
+ }
115
+ });
116
+ return `<nav class="crumbs">${crumbs.join(' <span class="crumb-sep">/</span> ')}</nav>`;
117
+ }
118
+ function renderSitemap(pages, currentPath) {
119
+ const root = { name: "", pages: [], subfolders: new Map() };
120
+ for (const p of pages) {
121
+ // index.md at any depth is the folder's homepage, not a sitemap child.
122
+ // The folder is already represented by its <details> wrapper in the parent.
123
+ if (p.path === "index.md" || p.path.endsWith("/index.md"))
124
+ continue;
125
+ const parts = p.path.split("/");
126
+ let node = root;
127
+ for (let i = 0; i < parts.length - 1; i++) {
128
+ const folder = parts[i];
129
+ let child = node.subfolders.get(folder);
130
+ if (!child) {
131
+ child = { name: folder, pages: [], subfolders: new Map() };
132
+ node.subfolders.set(folder, child);
133
+ }
134
+ node = child;
135
+ }
136
+ node.pages.push(p);
137
+ }
138
+ return `<nav><h4>Explorer</h4><ul class="sitemap-list">${renderNode(root, "", currentPath)}</ul></nav>`;
139
+ }
140
+ function renderNode(node, parentPath, currentPath) {
141
+ let html = "";
142
+ // Folders first, then pages — matches Obsidian's file explorer convention.
143
+ // Natural sort so "Page 2" comes before "Page 10" (instead of alphabetical).
144
+ for (const [name, sub] of [...node.subfolders].sort((a, b) => natCompare(a[0], b[0]))) {
145
+ const folderPath = parentPath ? `${parentPath}/${name}` : name;
146
+ const open = nodeContainsPath(sub, currentPath) ? " open" : "";
147
+ const href = "/" + folderPath.split("/").map(encodeURIComponent).join("/") + "/";
148
+ html += `<li class="sitemap-folder"><details${open}><summary><span class="folder-toggle" aria-hidden="true"></span><a href="${attr(href)}" class="folder-link">${esc(name)}</a></summary><ul class="sitemap-list">${renderNode(sub, folderPath, currentPath)}</ul></details></li>`;
149
+ }
150
+ for (const p of [...node.pages].sort((a, b) => natCompare(a.title, b.title))) {
151
+ html += sitemapItem(p, currentPath);
152
+ }
153
+ return html;
154
+ }
155
+ function natCompare(a, b) {
156
+ return a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" });
157
+ }
158
+ function nodeContainsPath(node, currentPath) {
159
+ if (node.pages.some((p) => p.path === currentPath))
160
+ return true;
161
+ for (const sub of node.subfolders.values()) {
162
+ if (nodeContainsPath(sub, currentPath))
163
+ return true;
164
+ }
165
+ return false;
166
+ }
167
+ function sitemapItem(p, currentPath) {
168
+ const href = "/" + p.path.replace(/\.md$/i, "").split("/").map(encodeURIComponent).join("/");
169
+ const cur = p.path === currentPath ? ' aria-current="page"' : "";
170
+ return `<li><a href="${attr(href)}"${cur} class="internal internal-link">${esc(p.title)}</a></li>`;
171
+ }
172
+ function esc(s) {
173
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
174
+ }
175
+ function attr(s) {
176
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
177
+ }
178
+ const HOVER_PREVIEW_SCRIPT = `<script>
179
+ (function () {
180
+ const cache = new Map();
181
+ let popover = null, showTimer = null, hideTimer = null, activeLink = null;
182
+ const HOVER_DELAY = 220, HIDE_DELAY = 180;
183
+
184
+ function ensurePopover() {
185
+ if (popover) return popover;
186
+ popover = document.createElement('div');
187
+ popover.className = 'wiki-preview';
188
+ popover.addEventListener('mouseenter', () => { if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; } });
189
+ popover.addEventListener('mouseleave', schedHide);
190
+ document.body.appendChild(popover);
191
+ return popover;
192
+ }
193
+ function position(pop, link) {
194
+ const rect = link.getBoundingClientRect();
195
+ pop.style.visibility = 'hidden'; pop.style.display = 'block';
196
+ const popRect = pop.getBoundingClientRect();
197
+ const m = 8;
198
+ let top = rect.bottom + window.scrollY + m;
199
+ if (rect.bottom + popRect.height + m > window.innerHeight) top = rect.top + window.scrollY - popRect.height - m;
200
+ let left = rect.left + window.scrollX;
201
+ if (left + popRect.width + m > window.innerWidth + window.scrollX) left = window.innerWidth + window.scrollX - popRect.width - m;
202
+ if (left < m) left = m;
203
+ pop.style.top = top + 'px'; pop.style.left = left + 'px';
204
+ pop.style.visibility = 'visible';
205
+ }
206
+ function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
207
+ function render(data, anchor) {
208
+ // .summary is already sanitised HTML at build time — safe to inject.
209
+ const section = anchor && data.headings && data.headings[anchor];
210
+ if (section) {
211
+ return '<div class="wiki-preview-title">' + esc(data.title) + '</div>' +
212
+ '<div class="wiki-preview-subheading">› ' + esc(section.title) + '</div>' +
213
+ '<div class="wiki-preview-body">' + (section.summary || '') + '</div>';
214
+ }
215
+ return '<div class="wiki-preview-title">' + esc(data.title) + '</div>' +
216
+ '<div class="wiki-preview-body">' + (data.summary || '') + '</div>';
217
+ }
218
+ async function fetchPreview(href) {
219
+ if (cache.has(href)) return cache.get(href);
220
+ try {
221
+ const url = href.replace(/#.*$/, '') + '.preview.json';
222
+ const res = await fetch(url);
223
+ if (!res.ok) { cache.set(href, null); return null; }
224
+ const data = await res.json();
225
+ cache.set(href, data);
226
+ return data;
227
+ } catch { cache.set(href, null); return null; }
228
+ }
229
+ function isInternal(el) {
230
+ if (!(el instanceof HTMLAnchorElement)) return false;
231
+ // Only preview links inside the main article body — sitemap / backlinks /
232
+ // breadcrumbs / TOC links shouldn't trigger popovers as the user navigates.
233
+ if (!el.closest('article')) return false;
234
+ const href = el.getAttribute('href');
235
+ if (!href || !href.startsWith('/') || href.startsWith('//')) return false;
236
+ if (href.endsWith('.json') || href.endsWith('.css')) return false;
237
+ return true;
238
+ }
239
+ function schedHide() { hideTimer = window.setTimeout(() => { if (popover) popover.style.display = 'none'; activeLink = null; }, HIDE_DELAY); }
240
+ document.addEventListener('mouseover', (e) => {
241
+ const link = e.target;
242
+ if (!isInternal(link)) return;
243
+ if (link === activeLink) { if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; } return; }
244
+ activeLink = link;
245
+ if (showTimer) clearTimeout(showTimer);
246
+ if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; }
247
+ showTimer = window.setTimeout(async () => {
248
+ const href = link.getAttribute('href');
249
+ const hashIdx = href.indexOf('#');
250
+ const anchor = hashIdx === -1 ? '' : href.slice(hashIdx + 1);
251
+ const data = await fetchPreview(href);
252
+ if (!data || activeLink !== link) return;
253
+ const pop = ensurePopover();
254
+ pop.innerHTML = render(data, anchor);
255
+ position(pop, link);
256
+ }, HOVER_DELAY);
257
+ });
258
+ document.addEventListener('mouseout', (e) => {
259
+ if (!isInternal(e.target)) return;
260
+ if (showTimer) { clearTimeout(showTimer); showTimer = null; }
261
+ schedHide();
262
+ });
263
+ })();
264
+ </script>`;
265
+ const TOC_SCRIPT = `<script>
266
+ (function () {
267
+ const headings = Array.from(document.querySelectorAll('article h2, article h3, article h4'));
268
+ const list = document.getElementById('page-toc');
269
+ if (!list || headings.length < 2) {
270
+ const toc = document.querySelector('.toc');
271
+ if (toc) toc.style.display = 'none';
272
+ return;
273
+ }
274
+ list.innerHTML = headings.map(h => {
275
+ const id = h.id || (h.querySelector('a') && h.querySelector('a').id) || '';
276
+ const text = h.textContent || '';
277
+ const cls = 'toc-d' + h.tagName[1];
278
+ return '<li class="' + cls + '"><a href="#' + id + '">' + text.replace(/[<>&]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;'}[c])) + '</a></li>';
279
+ }).join('');
280
+ })();
281
+ </script>`;
282
+ const AUTH_SCRIPT = `<script>
283
+ (function () {
284
+ const box = document.getElementById('vault-auth');
285
+ if (!box) return;
286
+ const role = readCookie('vault_role_display');
287
+ const next = encodeURIComponent(location.pathname + location.search + location.hash);
288
+ if (role) {
289
+ box.innerHTML =
290
+ '<div class="auth-status">Signed in as <strong>' + esc(role) + '</strong></div>' +
291
+ '<a class="auth-action" href="/logout?next=' + next + '">Sign out</a>';
292
+ } else {
293
+ box.innerHTML = '<a class="auth-action" href="/login.html?next=' + next + '">Sign in</a>';
294
+ }
295
+ function readCookie(name) {
296
+ for (const part of document.cookie.split(/;\\s*/)) {
297
+ const eq = part.indexOf('=');
298
+ if (eq > 0 && part.slice(0, eq) === name) return decodeURIComponent(part.slice(eq + 1));
299
+ }
300
+ return '';
301
+ }
302
+ function esc(s) { return String(s).replace(/[<>&]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;'}[c])); }
303
+ })();
304
+ </script>`;
305
+ const LIGHTBOX_SCRIPT = `<script>
306
+ (function () {
307
+ let onKey = null;
308
+ function close(overlay) {
309
+ overlay.remove();
310
+ if (onKey) document.removeEventListener('keydown', onKey);
311
+ onKey = null;
312
+ }
313
+ document.addEventListener('click', (e) => {
314
+ const img = e.target;
315
+ if (!(img instanceof HTMLImageElement)) return;
316
+ if (!img.closest('article')) return;
317
+ if (img.closest('a')) return; // skip linked images
318
+ e.preventDefault();
319
+ const overlay = document.createElement('div');
320
+ overlay.className = 'lightbox-overlay';
321
+ const big = document.createElement('img');
322
+ big.src = img.src;
323
+ big.alt = img.alt;
324
+ overlay.appendChild(big);
325
+ document.body.appendChild(overlay);
326
+ overlay.addEventListener('click', () => close(overlay));
327
+ onKey = (ev) => { if (ev.key === 'Escape') close(overlay); };
328
+ document.addEventListener('keydown', onKey);
329
+ });
330
+ })();
331
+ </script>`;
332
+ const SEARCH_SCRIPT = `<script>
333
+ (function () {
334
+ const input = document.getElementById('vault-search');
335
+ const results = document.querySelector('.search-results');
336
+ if (!input || !results) return;
337
+ let index = null;
338
+ let loading = false;
339
+
340
+ async function ensureIndex() {
341
+ if (index || loading) return;
342
+ loading = true;
343
+ try {
344
+ const res = await fetch('/_search-index.json');
345
+ if (res.ok) index = await res.json();
346
+ } finally { loading = false; }
347
+ }
348
+
349
+ function escape(s) { return String(s).replace(/[<>&]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;'}[c])); }
350
+
351
+ function buildSnippet(text, query) {
352
+ const idx = text.toLowerCase().indexOf(query);
353
+ if (idx === -1) return '';
354
+ const start = Math.max(0, idx - 50);
355
+ const end = Math.min(text.length, idx + query.length + 80);
356
+ let snippet = text.slice(start, end);
357
+ if (start > 0) snippet = '…' + snippet;
358
+ if (end < text.length) snippet = snippet + '…';
359
+ // Highlight the match.
360
+ const matchStart = idx - start + (start > 0 ? 1 : 0);
361
+ const before = escape(snippet.slice(0, matchStart));
362
+ const hit = escape(snippet.slice(matchStart, matchStart + query.length));
363
+ const after = escape(snippet.slice(matchStart + query.length));
364
+ return before + '<mark>' + hit + '</mark>' + after;
365
+ }
366
+
367
+ function show(matches) {
368
+ if (!matches.length) {
369
+ results.style.display = 'block';
370
+ results.innerHTML = '<div class="search-empty">No matches.</div>';
371
+ return;
372
+ }
373
+ results.style.display = 'block';
374
+ results.innerHTML = matches.slice(0, 25).map(m =>
375
+ '<a class="search-result" href="' + m.href.replace(/"/g, '&quot;') + '">' +
376
+ '<div class="search-result-title">' + escape(m.title) + '</div>' +
377
+ (m.folder ? '<div class="search-result-folder">' + escape(m.folder) + '</div>' : '') +
378
+ (m.snippet ? '<div class="search-result-summary">' + m.snippet + '</div>' : '') +
379
+ '</a>'
380
+ ).join('');
381
+ }
382
+
383
+ input.addEventListener('focus', ensureIndex);
384
+ input.addEventListener('input', async () => {
385
+ const q = input.value.trim().toLowerCase();
386
+ if (!q) { results.style.display = 'none'; return; }
387
+ await ensureIndex();
388
+ if (!index) return;
389
+ const matches = [];
390
+ for (const p of index) {
391
+ const titleLc = p.title.toLowerCase();
392
+ const pathLc = p.path.toLowerCase();
393
+ const textLc = (p.text || '').toLowerCase();
394
+ const inTitle = titleLc.includes(q);
395
+ const inPath = pathLc.includes(q);
396
+ const inText = textLc.includes(q);
397
+ if (!(inTitle || inPath || inText)) continue;
398
+ // Rank: title hits first, then path, then body. Stable order otherwise.
399
+ const rank = inTitle ? 0 : inPath ? 1 : 2;
400
+ matches.push({
401
+ ...p,
402
+ rank,
403
+ snippet: inText && !inTitle ? buildSnippet(p.text, q) : '',
404
+ });
405
+ }
406
+ matches.sort((a, b) => a.rank - b.rank);
407
+ show(matches);
408
+ });
409
+ document.addEventListener('click', (e) => {
410
+ if (e.target !== input && !results.contains(e.target)) results.style.display = 'none';
411
+ });
412
+ })();
413
+ </script>`;
414
+ //# sourceMappingURL=layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/render/layout.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,239 @@
1
+ // Pages Function for the MCP server. Reads from static assets via env.ASSETS
2
+ // — no R2 binding needed. Generated at build time so it ships alongside the
3
+ // auth middleware in dist/functions/.
4
+ //
5
+ // MCP tools (list_files, read_file, search_text, grep) operate on the .md
6
+ // source files we ship alongside the rendered .html. Role-aware: pulls the
7
+ // per-variant manifest matching the user's auth cookie.
8
+ export function renderMcpFunction(cfg) {
9
+ const rolesLiteral = JSON.stringify(cfg.roles);
10
+ return `// Auto-generated by the vaults CLI. Do not edit by hand.
11
+ const ROLES = ${rolesLiteral};
12
+ const COOKIE_NAME = "vault_role";
13
+ const PROTOCOL_VERSION = "2024-11-05";
14
+ const SERVER_INFO = { name: "vaults-template", version: "0.1.0" };
15
+
16
+ const TOOLS = [
17
+ {
18
+ name: "list_files",
19
+ description: "List markdown files in the vault. Optionally filter by path prefix.",
20
+ inputSchema: {
21
+ type: "object",
22
+ properties: { prefix: { type: "string" } },
23
+ },
24
+ },
25
+ {
26
+ name: "read_file",
27
+ description: "Read the full text content of a single vault markdown file.",
28
+ inputSchema: {
29
+ type: "object",
30
+ properties: { path: { type: "string" } },
31
+ required: ["path"],
32
+ },
33
+ },
34
+ {
35
+ name: "search_text",
36
+ description: "Plain substring search across vault markdown. Case-insensitive.",
37
+ inputSchema: {
38
+ type: "object",
39
+ properties: {
40
+ query: { type: "string" },
41
+ prefix: { type: "string" },
42
+ limit: { type: "number", default: 50 },
43
+ },
44
+ required: ["query"],
45
+ },
46
+ },
47
+ {
48
+ name: "grep",
49
+ description: "Regex search across vault markdown. JavaScript regex syntax.",
50
+ inputSchema: {
51
+ type: "object",
52
+ properties: {
53
+ pattern: { type: "string" },
54
+ flags: { type: "string", default: "" },
55
+ prefix: { type: "string" },
56
+ limit: { type: "number", default: 50 },
57
+ },
58
+ required: ["pattern"],
59
+ },
60
+ },
61
+ ];
62
+
63
+ export const onRequestPost = async (ctx) => {
64
+ const { request, env } = ctx;
65
+ let req;
66
+ try { req = await request.json(); } catch { return rpcError(null, -32700, "Parse error"); }
67
+ if (req.jsonrpc !== "2.0" || typeof req.method !== "string") {
68
+ return rpcError(req.id ?? null, -32600, "Invalid Request");
69
+ }
70
+
71
+ try {
72
+ switch (req.method) {
73
+ case "initialize":
74
+ return rpcResult(req.id, {
75
+ protocolVersion: PROTOCOL_VERSION,
76
+ capabilities: { tools: {} },
77
+ serverInfo: SERVER_INFO,
78
+ });
79
+ case "tools/list":
80
+ return rpcResult(req.id, { tools: TOOLS });
81
+ case "tools/call":
82
+ return await handleToolCall(req, ctx);
83
+ default:
84
+ return rpcError(req.id, -32601, "Method not found: " + req.method);
85
+ }
86
+ } catch (err) {
87
+ return rpcError(req.id, -32603, err instanceof Error ? err.message : "Internal error");
88
+ }
89
+ };
90
+
91
+ async function handleToolCall(req, ctx) {
92
+ const params = req.params ?? {};
93
+ const args = params.arguments ?? {};
94
+
95
+ // Determine which variant to read from based on the user's role cookie.
96
+ const role = await readRole(ctx.request, ctx.env);
97
+ const manifestUrl = new URL("/_variants/" + role + "/_manifest.json", ctx.request.url).toString();
98
+ const manifestRes = await ctx.env.ASSETS.fetch(manifestUrl);
99
+ let manifest;
100
+ if (manifestRes.ok) {
101
+ manifest = await manifestRes.json();
102
+ } else {
103
+ // Single-role build (no _variants prefix) — manifest sits at the root.
104
+ const rootUrl = new URL("/_manifest.json", ctx.request.url).toString();
105
+ const rootRes = await ctx.env.ASSETS.fetch(rootUrl);
106
+ if (!rootRes.ok) return rpcError(req.id, -32603, "Manifest not found");
107
+ manifest = await rootRes.json();
108
+ }
109
+
110
+ // The MCP tools work on .md sources only.
111
+ const mdFiles = manifest.files.filter((f) => f.path.endsWith(".md"));
112
+
113
+ switch (params.name) {
114
+ case "list_files": {
115
+ const prefix = String(args.prefix ?? "");
116
+ const matched = prefix
117
+ ? mdFiles.filter((f) => f.path.startsWith(prefix))
118
+ : mdFiles;
119
+ return toolResult(req.id, matched.map((f) => f.path + " (" + f.size + " bytes)").join("\\n"));
120
+ }
121
+
122
+ case "read_file": {
123
+ const path = String(args.path ?? "");
124
+ if (!path) return rpcError(req.id, -32602, "path is required");
125
+ const exists = mdFiles.some((f) => f.path === path);
126
+ if (!exists) return rpcError(req.id, -32602, "File not found: " + path);
127
+ const fileUrl = new URL("/" + path, ctx.request.url).toString();
128
+ const res = await ctx.env.ASSETS.fetch(fileUrl);
129
+ if (!res.ok) return rpcError(req.id, -32603, "Read failed: " + path);
130
+ return toolResult(req.id, await res.text());
131
+ }
132
+
133
+ case "search_text":
134
+ case "grep": {
135
+ const query = String(args.query ?? args.pattern ?? "");
136
+ if (!query) return rpcError(req.id, -32602, (params.name === "grep" ? "pattern" : "query") + " is required");
137
+
138
+ let matcher;
139
+ if (params.name === "grep") {
140
+ try { matcher = new RegExp(query, String(args.flags ?? "")); }
141
+ catch (err) { return rpcError(req.id, -32602, "Invalid regex: " + (err instanceof Error ? err.message : "?")); }
142
+ } else {
143
+ const lc = query.toLowerCase();
144
+ matcher = { test: (s) => s.toLowerCase().includes(lc) };
145
+ }
146
+
147
+ const prefix = String(args.prefix ?? "");
148
+ const limit = Math.min(Number(args.limit ?? 50), 200);
149
+ const candidates = prefix ? mdFiles.filter((f) => f.path.startsWith(prefix)) : mdFiles;
150
+
151
+ const results = [];
152
+ outer: for (const f of candidates) {
153
+ const fileUrl = new URL("/" + f.path, ctx.request.url).toString();
154
+ const res = await ctx.env.ASSETS.fetch(fileUrl);
155
+ if (!res.ok) continue;
156
+ const text = await res.text();
157
+ const lines = text.split("\\n");
158
+ for (let i = 0; i < lines.length; i++) {
159
+ if (matcher.test(lines[i])) {
160
+ const line = lines[i].trim();
161
+ results.push(f.path + ":" + (i + 1) + ": " + (line.length > 200 ? line.slice(0, 200) + "…" : line));
162
+ if (results.length >= limit) break outer;
163
+ }
164
+ }
165
+ }
166
+ return toolResult(req.id, results.length === 0 ? "No matches." : results.join("\\n"));
167
+ }
168
+
169
+ default:
170
+ return rpcError(req.id, -32602, "Unknown tool: " + params.name);
171
+ }
172
+ }
173
+
174
+ async function readRole(request, env) {
175
+ const fallback = ROLES[0] ?? "public";
176
+ if (!env.SESSION_SECRET) return fallback;
177
+
178
+ // Authorization: Bearer <token> first (curl, MCP-over-HTTP clients).
179
+ const auth = request.headers.get("Authorization") || "";
180
+ const bearerMatch = /^Bearer\\s+(.+)$/i.exec(auth);
181
+ if (bearerMatch) {
182
+ const role = await verifyToken(bearerMatch[1], env.SESSION_SECRET);
183
+ if (role && ROLES.includes(role)) return role;
184
+ }
185
+
186
+ // ?_token=<token> — keeps cross-origin GETs CORS-simple (no preflight).
187
+ const queryToken = new URL(request.url).searchParams.get("_token");
188
+ if (queryToken) {
189
+ const role = await verifyToken(queryToken, env.SESSION_SECRET);
190
+ if (role && ROLES.includes(role)) return role;
191
+ }
192
+
193
+ // Then cookie (browser-based MCP clients, if any).
194
+ const cookieHeader = request.headers.get("Cookie") || "";
195
+ const cookies = {};
196
+ for (const part of cookieHeader.split(/;\\s*/)) {
197
+ const eq = part.indexOf("=");
198
+ if (eq > 0) cookies[part.slice(0, eq)] = part.slice(eq + 1);
199
+ }
200
+ const cookie = cookies[COOKIE_NAME];
201
+ if (!cookie) return fallback;
202
+ const role = await verifyToken(cookie, env.SESSION_SECRET);
203
+ return role && ROLES.includes(role) ? role : fallback;
204
+ }
205
+
206
+ async function verifyToken(token, secretHex) {
207
+ const parts = token.split(".");
208
+ if (parts.length !== 3) return null;
209
+ const [role, expStr, sig] = parts;
210
+ const exp = Number(expStr);
211
+ if (!Number.isFinite(exp) || exp < Math.floor(Date.now() / 1000)) return null;
212
+ const keyBytes = new Uint8Array(secretHex.length / 2);
213
+ for (let i = 0; i < keyBytes.length; i++) keyBytes[i] = parseInt(secretHex.slice(i * 2, i * 2 + 2), 16);
214
+ const key = await crypto.subtle.importKey("raw", keyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
215
+ const macBytes = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(role + "." + expStr));
216
+ let macHex = "";
217
+ for (const b of new Uint8Array(macBytes)) macHex += b.toString(16).padStart(2, "0");
218
+ if (macHex.length !== sig.length) return null;
219
+ let diff = 0;
220
+ for (let i = 0; i < macHex.length; i++) diff |= macHex.charCodeAt(i) ^ sig.charCodeAt(i);
221
+ return diff === 0 ? role : null;
222
+ }
223
+
224
+ function rpcResult(id, result) {
225
+ return new Response(JSON.stringify({ jsonrpc: "2.0", id, result }), {
226
+ headers: { "Content-Type": "application/json" },
227
+ });
228
+ }
229
+ function rpcError(id, code, message) {
230
+ return new Response(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }), {
231
+ headers: { "Content-Type": "application/json" },
232
+ });
233
+ }
234
+ function toolResult(id, text) {
235
+ return rpcResult(id, { content: [{ type: "text", text }] });
236
+ }
237
+ `;
238
+ }
239
+ //# sourceMappingURL=mcp-template.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-template.js","sourceRoot":"","sources":["../../src/render/mcp-template.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,4EAA4E;AAC5E,sCAAsC;AACtC,EAAE;AACF,0EAA0E;AAC1E,2EAA2E;AAC3E,wDAAwD;AAOxD,MAAM,UAAU,iBAAiB,CAAC,GAAsB;IACtD,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAE/C,OAAO;gBACO,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkO3B,CAAC;AACF,CAAC"}