llmd 0.2.2 → 0.4.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 CHANGED
@@ -25,6 +25,7 @@ That's it! The docs will open in your browser. Click through the sidebar to expl
25
25
  - **Live reload** - Watch mode reloads on file changes
26
26
  - **Copy buttons** - One-click code copying
27
27
  - **Table of contents** - Auto-generated from headings
28
+ - **Highlights** - Extract and save important passages from your docs
28
29
  - **Usage Analytics** - Track which docs you view most (local-only, opt-in)
29
30
 
30
31
  ## Documentation
@@ -33,6 +34,7 @@ That's it! The docs will open in your browser. Click through the sidebar to expl
33
34
  - [Usage](./docs/usage.md) - Command-line options and examples
34
35
  - [Themes](./docs/themes.md) - Built-in and custom color themes
35
36
  - [Fonts](./docs/fonts.md) - Built-in and custom font combinations
37
+ - [Highlights](./docs/highlights.md) - Extract and save important passages
36
38
  - [Analytics](./docs/analytics.md) - Local usage tracking
37
39
 
38
40
  ## Basic Usage
@@ -52,6 +54,11 @@ llmd ./docs --theme dark --watch
52
54
 
53
55
  # Open directly to analytics
54
56
  llmd analytics
57
+
58
+ # Manage analytics database
59
+ llmd db check # View database stats
60
+ llmd db cleanup --days 30 # Delete old events
61
+ llmd db clear # Clear all analytics data
55
62
  ```
56
63
 
57
64
  See [Usage](./docs/usage.md) for all options.
package/dist/client.js CHANGED
@@ -1 +1,53 @@
1
- var Q=()=>{let y=document.querySelectorAll(".dir-label");for(let m of Array.from(y)){let C=m,L=document.createElement("span");L.className="dir-chevron",L.innerHTML='<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>',C.insertBefore(L,C.firstChild),C.style.cursor="pointer",C.addEventListener("click",(q)=>{q.preventDefault();let B=C.closest(".dir-item");if(B)B.classList.toggle("collapsed")})}},Y=()=>{let y=document.querySelector(".toc");if(!y)return;let m=y.querySelector("h3");if(!m)return;let C=document.createElement("span");C.className="toc-chevron",C.innerHTML='<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>',m.style.cursor="pointer",m.insertBefore(C,m.firstChild),y.classList.add("collapsed"),m.addEventListener("click",()=>{y.classList.toggle("collapsed")})};if(typeof window<"u")window.addEventListener("DOMContentLoaded",()=>{Q(),Y()});var u=()=>{for(let y of Array.from(document.querySelectorAll("pre code"))){let m=y.parentElement;if(!m)continue;if(m.querySelector(".copy-button"))continue;let C=document.createElement("button");C.className="copy-button",C.textContent="Copy",C.setAttribute("aria-label","Copy code to clipboard"),C.addEventListener("click",async()=>{try{await navigator.clipboard.writeText(y.textContent||""),C.textContent="Copied!",C.classList.add("copied"),setTimeout(()=>{C.textContent="Copy",C.classList.remove("copied")},2000)}catch(L){console.error("Failed to copy:",L),C.textContent="Failed",setTimeout(()=>{C.textContent="Copy"},2000)}}),m.appendChild(C)}};if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",u);else u();var Z=(y)=>{let C=`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/_ws`,L=null,q=null,B=()=>{L=new WebSocket(C),L.addEventListener("open",()=>{console.log("[llmd] Connected to file watcher"),L?.send(JSON.stringify({type:"watch",file:y}))}),L.addEventListener("message",(M)=>{try{let x=JSON.parse(M.data);if(x.type==="reload"&&x.file===y)console.log(`[llmd] File changed: ${y}, reloading...`),window.location.reload()}catch(x){console.error("[llmd] Failed to parse message:",x)}}),L.addEventListener("close",()=>{console.log("[llmd] Disconnected from file watcher"),q=window.setTimeout(()=>{console.log("[llmd] Reconnecting..."),B()},2000)}),L.addEventListener("error",(M)=>{console.error("[llmd] WebSocket error:",M),L?.close()})};B(),window.addEventListener("beforeunload",()=>{if(q)clearTimeout(q);L?.close()})};window.connectFileWatcher=Z;var _=()=>{let y=document.querySelector(".sidebar");if(!y)return;let m=document.createElement("div");m.className="sidebar-resize-handle",y.appendChild(m);let C=!1,L=0,q=0,B=(A)=>{C=!0,L=A.clientX,q=y.offsetWidth,document.body.style.cursor="ew-resize",document.body.style.userSelect="none",A.preventDefault()},M=(A)=>{if(!C)return;let K=A.clientX-L,O=q+K,k=Math.max(200,Math.min(600,O));y.style.width=`${k}px`,localStorage.setItem("llmd-sidebar-width",k.toString())},x=()=>{if(!C)return;C=!1,document.body.style.cursor="",document.body.style.userSelect=""};m.addEventListener("mousedown",B),document.addEventListener("mousemove",M),document.addEventListener("mouseup",x);let j=localStorage.getItem("llmd-sidebar-width");if(j){let A=Number.parseInt(j,10);if(A>=200&&A<=600)y.style.width=`${A}px`}};if(typeof window<"u")window.addEventListener("DOMContentLoaded",()=>{_()});var E=(y,m,C)=>{fetch("/api/events",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:y,path:m,resourceType:C})}).catch(()=>{})},G=(y)=>{E("open",y,"dir")},J=(y)=>{E("view",y,"file")};window.trackDirectoryOpen=G;window.trackFileView=J;console.log("[llmd] Client initialized");
1
+ var R=()=>{let C=document.querySelector(".admin-section"),J=document.querySelector(".admin-header");if(!(C&&J))return;if(localStorage.getItem("llmd-admin-collapsed")==="true")C.classList.add("collapsed");J.addEventListener("click",()=>{let j=C.classList.toggle("collapsed");localStorage.setItem("llmd-admin-collapsed",j.toString())})};var k=/\/$/,w=(C)=>{let J=C.querySelector(":scope > .dir-label");if(!J)return null;let q=Array.from(J.querySelectorAll("span"));if(q.length===0)return null;let j=q[q.length-1];if(!j.textContent)return null;return j.textContent.replace(k,"")},T=(C)=>{let J=[],q=C;while(q){let j=w(q);if(j)J.unshift(j);let K=q.parentElement;q=K?K.closest(".dir-item"):null}return J.length>0?J.join("/"):null},v=()=>{let C=[],J=document.querySelectorAll(".dir-item.collapsed");for(let q of Array.from(J)){let j=T(q);if(j)C.push(j)}localStorage.setItem("llmd-nav-collapsed",JSON.stringify(C))},I=()=>{try{let C=localStorage.getItem("llmd-nav-collapsed");if(!C)return;let J=JSON.parse(C),q=document.querySelectorAll(".dir-item");for(let j of Array.from(q)){let K=T(j);if(K&&J.includes(K))j.classList.add("collapsed")}}catch(C){console.error("[collapsible] Failed to restore state:",C)}},P=()=>{let C=document.querySelectorAll(".dir-label");for(let J of Array.from(C)){let q=J,j=document.createElement("span");j.className="dir-chevron",j.innerHTML='<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>',q.insertBefore(j,q.firstChild),q.style.cursor="pointer",q.addEventListener("click",(K)=>{K.preventDefault();let M=q.closest(".dir-item");if(M)M.classList.toggle("collapsed"),v()})}I()},f=()=>{let C=document.querySelector(".toc");if(!C)return;let J=C.querySelector("h3");if(!J)return;let q=document.createElement("span");q.className="toc-chevron",q.innerHTML='<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>',J.style.cursor="pointer",J.insertBefore(q,J.firstChild),J.addEventListener("click",()=>{C.classList.toggle("collapsed")})};if(typeof window<"u")window.addEventListener("DOMContentLoaded",()=>{P(),f()});var z=()=>{for(let C of Array.from(document.querySelectorAll("pre code"))){let J=C.parentElement;if(!J)continue;if(J.querySelector(".copy-button"))continue;let q=document.createElement("button");q.className="copy-button",q.textContent="Copy",q.setAttribute("aria-label","Copy code to clipboard"),q.addEventListener("click",async()=>{try{await navigator.clipboard.writeText(C.textContent||""),q.textContent="Copied!",q.classList.add("copied"),setTimeout(()=>{q.textContent="Copy",q.classList.remove("copied")},2000)}catch(j){console.error("Failed to copy:",j),q.textContent="Failed",setTimeout(()=>{q.textContent="Copy"},2000)}}),J.appendChild(q)}};if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",z);else z();var m=(C)=>{let q=`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/_ws`,j=null,K=null,M=()=>{j=new WebSocket(q),j.addEventListener("open",()=>{console.log("[llmd] Connected to file watcher"),j?.send(JSON.stringify({type:"watch",file:C}))}),j.addEventListener("message",(Q)=>{try{let Z=JSON.parse(Q.data);if(Z.type==="reload"&&Z.file===C)console.log(`[llmd] File changed: ${C}, reloading...`),window.location.reload()}catch(Z){console.error("[llmd] Failed to parse message:",Z)}}),j.addEventListener("close",()=>{console.log("[llmd] Disconnected from file watcher"),K=window.setTimeout(()=>{console.log("[llmd] Reconnecting..."),M()},2000)}),j.addEventListener("error",(Q)=>{console.error("[llmd] WebSocket error:",Q),j?.close()})};M(),window.addEventListener("beforeunload",()=>{if(K)clearTimeout(K);j?.close()})};window.connectFileWatcher=m;var F=[],G=null,B=null,y=new Map,A=()=>{if(!document.querySelector(".content"))return;u(),document.addEventListener("mouseup",b),document.addEventListener("touchend",b)},u=async()=>{try{let C=window.location.pathname.replace("/view/",""),J=await fetch(`/api/highlights/resource?path=${encodeURIComponent(C)}`);if(!J.ok)return;F=(await J.json()).highlights||[],x()}catch(C){console.error("[highlights] Failed to fetch highlights:",C)}},b=(C)=>{let J=C.target;if(G&&G.contains(J))return;let q=window.getSelection();if(!q||q.isCollapsed){L();return}let j=q.toString().trim();if(!j){L();return}let K=document.querySelector(".content");if(!K)return;let M=q.getRangeAt(0),Q=M.commonAncestorContainer,Z=Q.nodeType===Node.TEXT_NODE?Q.parentElement:Q;if(!(Z&&K.contains(Z))){L();return}B={text:j,range:M.cloneRange()},d(C)},d=(C)=>{if(!G)G=p(),document.body.appendChild(G);let{pageX:J,pageY:q}=C;G.style.left=`${J}px`,G.style.top=`${q-50}px`,G.style.display="block"},L=()=>{if(G)G.style.display="none";B=null},c=(C,J)=>{let q=y.get(C.id);if(q){q.remove(),y.delete(C.id);return}let j=document.createElement("div");j.className="highlight-notes-popup",j.style.cssText=`
2
+ position: absolute;
3
+ background: var(--bg);
4
+ border: 1px solid var(--border);
5
+ border-radius: 6px;
6
+ padding: 12px;
7
+ max-width: 300px;
8
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
9
+ z-index: 10000;
10
+ font-size: 13px;
11
+ line-height: 1.5;
12
+ `;let K=document.createElement("div");K.style.cssText="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;";let M=document.createElement("span");M.textContent="Highlight Notes",M.style.cssText="font-weight: 600; color: var(--fg);";let Q=document.createElement("button");Q.textContent="×",Q.style.cssText="background: none; border: none; color: var(--fg); cursor: pointer; padding: 0; font-size: 18px; line-height: 1; opacity: 0.7;",Q.addEventListener("click",(Y)=>{Y.stopPropagation(),j.remove(),y.delete(C.id)}),K.appendChild(M),K.appendChild(Q);let Z=document.createElement("div");Z.style.cssText="color: var(--fg); white-space: pre-wrap; word-wrap: break-word;",Z.textContent=C.notes||"(No notes)",j.appendChild(K),j.appendChild(Z),j.addEventListener("click",(Y)=>{Y.stopPropagation()}),j.style.left=`${J.pageX}px`,j.style.top=`${J.pageY+10}px`,document.body.appendChild(j),y.set(C.id,j);let $=(Y)=>{if(!j.contains(Y.target))j.remove(),y.delete(C.id),document.removeEventListener("click",$)};setTimeout(()=>{document.addEventListener("click",$)},0)},p=()=>{let C=document.createElement("div");C.className="highlight-popup",C.innerHTML=`
13
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
14
+ <span style="font-size: 13px; font-weight: 500; color: var(--fg);">Add Highlight</span>
15
+ <button class="highlight-close-btn" type="button" style="background: none; border: none; color: var(--fg); cursor: pointer; padding: 0; font-size: 18px; line-height: 1; opacity: 0.7;">&times;</button>
16
+ </div>
17
+ <textarea
18
+ class="highlight-notes-input"
19
+ placeholder="Add a note (optional)"
20
+ rows="2"
21
+ style="width: 100%; padding: 8px; border: 1px solid var(--border); border-radius: 4px; background: var(--bg); color: var(--fg); font-size: 13px; resize: vertical; margin-bottom: 8px; font-family: inherit;"
22
+ ></textarea>
23
+ <button class="highlight-create-btn" type="button">
24
+ Create Highlight
25
+ </button>
26
+ `;let J=C.querySelector(".highlight-create-btn");if(J)J.addEventListener("click",async(j)=>{j.stopPropagation(),await g()});let q=C.querySelector(".highlight-close-btn");if(q)q.addEventListener("click",(j)=>{j.stopPropagation(),L()});return C},g=async()=>{if(!B)return;try{let C=document.querySelector(".content");if(!C)return;let J=C.textContent||"",q=B.range,j=document.createRange();j.selectNodeContents(C),j.setEnd(q.startContainer,q.startOffset);let K=j.toString().length,M=K+B.text.length,Q=G?.querySelector(".highlight-notes-input"),Z=Q?.value.trim()||void 0,$=window.location.pathname.replace("/view/",""),Y=await fetch("/api/highlights",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({resourcePath:$,startOffset:K,endOffset:M,highlightedText:B.text,notes:Z})});if(!Y.ok)throw Error("Failed to create highlight");let _=await Y.json();if(F.push({id:_.id,startOffset:K,endOffset:M,highlightedText:B.text,isStale:!1,notes:Z||null,createdAt:Date.now()}),x(),Q)Q.value="";L(),window.getSelection()?.removeAllRanges()}catch(C){console.error("[highlights] Failed to create highlight:",C)}},x=()=>{let C=document.querySelector(".content");if(!C)return;let J=C.querySelectorAll("mark.llmd-highlight");for(let K of J){let M=K.parentNode;if(M)M.replaceChild(document.createTextNode(K.textContent||""),K)}if(C.normalize(),F.length===0)return;let q=[...F].sort((K,M)=>M.startOffset-K.startOffset),j=C.textContent||"";for(let K of q)try{let M=document.createTreeWalker(C,NodeFilter.SHOW_TEXT),Q=0,Z=null,$=0,Y=null,_=0,U=M.nextNode();while(U){let D=U.textContent?.length||0,W=Q+D;if(Z===null&&Q<=K.startOffset&&K.startOffset<W)Z=U,$=K.startOffset-Q;if(Q<K.endOffset&&K.endOffset<=W){Y=U,_=K.endOffset-Q;break}Q=W,U=M.nextNode()}if(!(Z&&Y))continue;let V=document.createRange();V.setStart(Z,$),V.setEnd(Y,_);let X=document.createElement("mark");X.className=K.isStale?"llmd-highlight llmd-highlight-stale":"llmd-highlight",X.dataset.highlightId=K.id,X.title=K.isStale?"This highlight may be outdated":"",X.style.cursor="pointer",X.addEventListener("click",(D)=>{D.stopPropagation(),c(K,D)}),V.surroundContents(X)}catch(M){console.error("[highlights] Failed to render highlight:",M)}};var o=(C)=>{return new Date(C).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit"})},l=(C)=>{let J=document.querySelector(`[data-highlight-id="${C}"]`);if(J)J.scrollIntoView({behavior:"smooth",block:"center"}),J.classList.add("highlight-flash"),setTimeout(()=>{J.classList.remove("highlight-flash")},1000)},a=(C,J)=>C.length>J?`${C.slice(0,J)}...`:C,O=()=>{let C=document.querySelector(".content");if(!C)return;let J=window.location.pathname;if(!J.startsWith("/view/"))return;let q=J.slice(6);fetch(`/api/highlights/resource?path=${encodeURIComponent(q)}`).then((j)=>j.json()).then((j)=>{let K=j.highlights||[];if(K.length===0)return;let M=document.createElement("div");M.className="highlights-summary",M.innerHTML=`
27
+ <div style="background: var(--code-bg); border: 1px solid var(--border); border-radius: 8px; padding: 16px 20px; margin: 24px 0;">
28
+ <h3 style="font-size: 0.875rem; margin: 0 0 12px 0; text-transform: uppercase; letter-spacing: 0.08em; font-weight: 700; opacity: 0.8; display: flex; align-items: center; gap: 6px;">
29
+ <span style="font-size: 16px;">✨</span>
30
+ Highlights (${K.length})
31
+ </h3>
32
+ <ul style="list-style: none; padding: 0; margin: 0; font-size: 14px;">
33
+ ${K.map(($)=>{let Y=$.isStale?'<span style="color: #f44336; margin-left: 4px;" title="Stale - file has changed">⚠️</span>':"",_=$.isStale?' style="text-decoration: line-through; opacity: 0.5;"':"",U=$.highlightedText.length>80?`${$.highlightedText.slice(0,80)}...`:$.highlightedText,V=$.notes?a($.notes,60):"",X=V?`<div style="margin-top: 4px; padding: 6px; background: var(--sidebar-bg); border-radius: 3px; font-size: 11px; opacity: 0.8;">\uD83D\uDCAD ${V}</div>`:"";return`
34
+ <li style="margin: 8px 0; padding: 8px; background: var(--bg); border-radius: 4px; border-left: 3px solid var(--accent); cursor: pointer; transition: background 0.2s;"
35
+ data-highlight-id="${$.id}"
36
+ class="highlight-summary-item"
37
+ onmouseover="this.style.background='var(--hover)'"
38
+ onmouseout="this.style.background='var(--bg)'">
39
+ <div${_}>${U}</div>
40
+ ${X}
41
+ <div style="opacity: 0.5; font-size: 12px; margin-top: 4px;">
42
+ ${o($.createdAt)}${Y}
43
+ </div>
44
+ </li>
45
+ `}).join("")}
46
+ </ul>
47
+ <div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border);">
48
+ <a href="/highlights" style="color: var(--accent); text-decoration: none; font-size: 13px; font-weight: 500;">
49
+ View all highlights →
50
+ </a>
51
+ </div>
52
+ </div>
53
+ `;let Q=C.querySelector("h1, h2");if(Q)Q.parentNode?.insertBefore(M,Q);else C.insertBefore(M,C.firstChild);let Z=M.querySelectorAll(".highlight-summary-item");for(let $ of Array.from(Z)){let Y=$.getAttribute("data-highlight-id");if(Y)$.addEventListener("click",()=>l(Y))}}).catch((j)=>{console.error("Failed to load highlights summary:",j)})};var s=()=>{let C=document.querySelector(".sidebar");if(!C)return;let J=document.createElement("div");J.className="sidebar-resize-handle",C.appendChild(J);let q=!1,j=0,K=0,M=(Y)=>{q=!0,j=Y.clientX,K=C.offsetWidth,document.body.style.cursor="ew-resize",document.body.style.userSelect="none",Y.preventDefault()},Q=(Y)=>{if(!q)return;let _=Y.clientX-j,U=K+_,V=Math.max(200,Math.min(600,U));C.style.width=`${V}px`,localStorage.setItem("llmd-sidebar-width",V.toString())},Z=()=>{if(!q)return;q=!1,document.body.style.cursor="",document.body.style.userSelect=""};J.addEventListener("mousedown",M),document.addEventListener("mousemove",Q),document.addEventListener("mouseup",Z);let $=localStorage.getItem("llmd-sidebar-width");if($){let Y=Number.parseInt($,10);if(Y>=200&&Y<=600)C.style.width=`${Y}px`}};if(typeof window<"u")window.addEventListener("DOMContentLoaded",()=>{s()});var S=(C,J,q)=>{fetch("/api/events",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:C,path:J,resourceType:q})}).catch(()=>{})},N=(C)=>{S("open",C,"dir")},H=(C)=>{S("view",C,"file")};var E=async()=>{try{let C=window.location.pathname.replace("/view/",""),J=C.includes("/")?C.substring(0,C.lastIndexOf("/")):"",q=await fetch(`/api/highlights/directory?path=${encodeURIComponent(J||".")}`);if(!q.ok)return;let K=(await q.json()).highlights||[],M=new Set;for(let Z of K)M.add(Z.resourcePath);let Q=document.querySelectorAll("a[data-file-path]");for(let Z of Array.from(Q)){let $=Z.getAttribute("data-file-path");if($&&M.has($)){let Y=Z.querySelector(".file-icon svg");if(Y)Y.setAttribute("fill","var(--accent)"),Y.setAttribute("fill-opacity","0.3")}}}catch(C){console.error("[file-highlights] Failed to load highlight indicators:",C)}};if(typeof window<"u")window.addEventListener("DOMContentLoaded",()=>{E()});window.trackDirectoryOpen=N;window.trackFileView=H;R();E();if(document.querySelector(".content"))A(),O();console.log("[llmd] Client initialized");