llmd 0.2.1 → 0.2.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 +2 -19
- package/dist/llmd +6 -6
- package/package.json +3 -6
package/README.md
CHANGED
|
@@ -1,22 +1,8 @@
|
|
|
1
1
|
# llmd
|
|
2
2
|
|
|
3
|
-
**Serve Markdown as
|
|
3
|
+
**Serve Markdown as HTML, instantly.**
|
|
4
4
|
|
|
5
|
-
A minimal CLI tool for viewing Markdown files in your browser with syntax highlighting, live reload, and
|
|
6
|
-
|
|
7
|
-
## Installation
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npm install -g llmd
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Or run directly without installing:
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
npx llmd
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Requires Node.js 22 or later.
|
|
5
|
+
A minimal CLI tool for viewing Markdown files in your browser with syntax highlighting, live reload, and optional event analytics. Built for developers reviewing LLM-generated documentation.
|
|
20
6
|
|
|
21
7
|
## Quick Start
|
|
22
8
|
|
|
@@ -38,9 +24,6 @@ That's it! The docs will open in your browser. Click through the sidebar to expl
|
|
|
38
24
|
- **Syntax highlighting** - Powered by Shiki
|
|
39
25
|
- **Live reload** - Watch mode reloads on file changes
|
|
40
26
|
- **Copy buttons** - One-click code copying
|
|
41
|
-
- **Dark/light themes** - With 9 font combinations
|
|
42
|
-
- **Fast** - Built with Bun, instant startup
|
|
43
|
-
- **Sidebar navigation** - Browse files with directory structure
|
|
44
27
|
- **Table of contents** - Auto-generated from headings
|
|
45
28
|
- **Usage Analytics** - Track which docs you view most (local-only, opt-in)
|
|
46
29
|
|
package/dist/llmd
CHANGED
|
@@ -727,7 +727,7 @@ import{createRequire as Ip}from"node:module";var Wp=Object.create;var{getPrototy
|
|
|
727
727
|
</div>
|
|
728
728
|
`:""}
|
|
729
729
|
</div>
|
|
730
|
-
`},Bh=(e)=>{let{data:n,config:t,files:a,clientScript:i,showAllHistory:c=!1}=e,s=Gh(n,c);return nt({content:s,title:"Analytics",theme:t.theme,fontTheme:t.fontTheme,files:a,clientScript:i})};var Zp=_n(()=>{oc()});import{execSync as nf}from"child_process";import{existsSync as tf}from"fs";import{homedir as af}from"os";import{join as Up}from"path";import{spawn as Lp}from"node:child_process";var la=(e)=>{let n=process.platform;try{let t,a;if(n==="darwin")t="open",a=[e];else if(n==="win32")t="cmd",a=["/c","start",e];else t="xdg-open",a=[e];Lp(t,a,{detached:!0,stdio:"ignore"}).unref()}catch(t){console.error("Failed to open browser:",t)}};import{existsSync as fm}from"node:fs";import{dirname as ym,isAbsolute as wm,resolve as xm}from"node:path";var dc={name:"llmd",version:"0.2.
|
|
730
|
+
`},Bh=(e)=>{let{data:n,config:t,files:a,clientScript:i,showAllHistory:c=!1}=e,s=Gh(n,c);return nt({content:s,title:"Analytics",theme:t.theme,fontTheme:t.fontTheme,files:a,clientScript:i})};var Zp=_n(()=>{oc()});import{execSync as nf}from"child_process";import{existsSync as tf}from"fs";import{homedir as af}from"os";import{join as Up}from"path";import{spawn as Lp}from"node:child_process";var la=(e)=>{let n=process.platform;try{let t,a;if(n==="darwin")t="open",a=[e];else if(n==="win32")t="cmd",a=["/c","start",e];else t="xdg-open",a=[e];Lp(t,a,{detached:!0,stdio:"ignore"}).unref()}catch(t){console.error("Failed to open browser:",t)}};import{existsSync as fm}from"node:fs";import{dirname as ym,isAbsolute as wm,resolve as xm}from"node:path";var dc={name:"llmd",version:"0.2.2",description:"Local markdown server for LLM-generated docs",author:"Phil Zona <phil.b.zona@gmail.com>",license:"MIT",repository:{type:"git",url:"https://github.com/pbzona/llmd.git"},homepage:"https://github.com/pbzona/llmd#readme",bugs:{url:"https://github.com/pbzona/llmd/issues"},type:"module",bin:{llmd:"./dist/llmd"},files:["dist/llmd","dist/client.js","LICENSE","README.md"],engines:{node:">=22"},publishConfig:{access:"public"},scripts:{dev:"bun --hot index.ts",test:"bun test",build:"bash scripts/build.sh",prepublishOnly:"bun test && bun run build",preview:"bun scripts/generate-preview.ts","check-contrast":"bun scripts/check-contrast.ts",format:"biome format --write .","format:check":"biome format .",lint:"biome lint .","lint:fix":"biome lint --write .",check:"biome check --write .",prepare:"husky"},keywords:["markdown","server","documentation","html","llm"],dependencies:{"@types/ws":"^8.18.1","fast-glob":"^3.3.3",figlet:"^1.9.4",libsql:"^0.5.22",marked:"^17.0.1",shiki:"^3.20.0",ws:"^8.18.3"},devDependencies:{"@biomejs/biome":"2.3.8","@types/bun":"latest","@types/figlet":"^1.7.0",husky:"^9.1.7",ultracite:"6.4.1"},peerDependencies:{typescript:"^5"}};import{randomUUID as Jp}from"node:crypto";import{existsSync as Qp,mkdirSync as Kp}from"node:fs";import{homedir as em}from"node:os";import{basename as fc,dirname as yc,join as ct}from"node:path";var nm=["node_modules",".git","dist","build","test-fixtures","preview-output"],wc=(e)=>{if(typeof Bun<"u"&&globalThis.Bun){let{Database:t}=Z("bun:sqlite");return new t(e)}return new(Z("libsql"))(e)},xc=()=>{let n=process.env.XDG_DATA_HOME||ct(em(),".local","share"),t=ct(n,"llmd");if(!Qp(t))Kp(t,{recursive:!0});return ct(t,"llmd.db")},da=(e,n)=>{if(e===n)return!1;return(e.startsWith(n)?e.slice(n.length+1):e).split("/").filter(Boolean).some((i)=>nm.includes(i))},gn=()=>Jp(),kc=(e)=>{e.exec("PRAGMA foreign_keys = ON"),e.exec(`
|
|
731
731
|
CREATE TABLE IF NOT EXISTS resources (
|
|
732
732
|
id TEXT PRIMARY KEY,
|
|
733
733
|
path TEXT UNIQUE NOT NULL,
|
|
@@ -752,7 +752,7 @@ import{createRequire as Ip}from"node:module";var Wp=Object.create;var{getPrototy
|
|
|
752
752
|
CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
|
|
753
753
|
CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
|
|
754
754
|
CREATE INDEX IF NOT EXISTS idx_resources_path ON resources(path);
|
|
755
|
-
`)},tm=async(e,n,t)=>{let{scanMarkdownFiles:a}=await Promise.resolve().then(() => (hc(),gc)),i=await a(n,10),c=e.prepare("INSERT OR IGNORE INTO resources (id, path, type, created_at) VALUES (?, ?, ?, ?)"),s=e.prepare("SELECT id FROM resources WHERE path = ?"),o=gn(),r=Date.now();c.run(o,n,"dir",r);let p=s.get(n);if(p)t.set(n,p.id);else t.set(n,o);let m=(d,_)=>{let g=gn(),h=Date.now();c.run(g,d,"dir",h);let w=s.get(d);if(w)t.set(d,w.id);else t.set(d,g);_.add(d)},u=(d)=>{let _=gn(),g=Date.now();c.run(_,d,"file",g);let h=s.get(d);if(h)t.set(d,h.id);else t.set(d,_)},l=(d,_)=>{let g=yc(d),h=[];while(g!==n&&!_.has(g)){if(!da(g,n))h.unshift(g);g=yc(g)}return h};e.transaction((d)=>{let _=new Set;for(let g of d){let h=ct(n,g.path);if(da(h,n))continue;let w=l(h,_);for(let y of w)m(y,_);u(h)}})(i)},am=(e)=>{try{let{statSync:a}=Z("node:fs"),i=a(e),c=(i.size/1024/1024).toFixed(2);if(i.size>52428800)console.warn(`[events] Database size is ${c}MB (threshold: 50MB)`),console.warn(`[events] Consider deleting old data: rm ${e}`)}catch(a){}},ba=(e,n)=>{try{return e.prepare("SELECT value FROM config WHERE key = ?").get(n)?.value??null}catch{return null}};var im=(e)=>{if(process.env.LLMD_ENABLE_EVENTS)return!0;return ba(e,"analytics_enabled")==="true"},$c=(e,n)=>{let t=n||xc();am(t);let a=wc(t);if(
|
|
755
|
+
`)},tm=async(e,n,t)=>{let{scanMarkdownFiles:a}=await Promise.resolve().then(() => (hc(),gc)),i=await a(n,10),c=e.prepare("INSERT OR IGNORE INTO resources (id, path, type, created_at) VALUES (?, ?, ?, ?)"),s=e.prepare("SELECT id FROM resources WHERE path = ?"),o=gn(),r=Date.now();c.run(o,n,"dir",r);let p=s.get(n);if(p)t.set(n,p.id);else t.set(n,o);let m=(d,_)=>{let g=gn(),h=Date.now();c.run(g,d,"dir",h);let w=s.get(d);if(w)t.set(d,w.id);else t.set(d,g);_.add(d)},u=(d)=>{let _=gn(),g=Date.now();c.run(_,d,"file",g);let h=s.get(d);if(h)t.set(d,h.id);else t.set(d,_)},l=(d,_)=>{let g=yc(d),h=[];while(g!==n&&!_.has(g)){if(!da(g,n))h.unshift(g);g=yc(g)}return h};e.transaction((d)=>{let _=new Set;for(let g of d){let h=ct(n,g.path);if(da(h,n))continue;let w=l(h,_);for(let y of w)m(y,_);u(h)}})(i)},am=(e)=>{try{let{statSync:a}=Z("node:fs"),i=a(e),c=(i.size/1024/1024).toFixed(2);if(i.size>52428800)console.warn(`[events] Database size is ${c}MB (threshold: 50MB)`),console.warn(`[events] Consider deleting old data: rm ${e}`)}catch(a){}},ba=(e,n)=>{try{return e.prepare("SELECT value FROM config WHERE key = ?").get(n)?.value??null}catch{return null}};var im=(e)=>{if(process.env.LLMD_ENABLE_EVENTS)return!0;return ba(e,"analytics_enabled")==="true"},$c=(e,n)=>{let t=n||xc();am(t);let a=wc(t);if(kc(a),!im(a))return a.close(),null;let i=new Map,c=[],s=!0,o=tm(a,e.directory,i).then(()=>{s=!1;for(let b of c)r(b.type,b.path,b.resourceType);c.length=0}).catch((b)=>{console.error("[events] Failed to scan resources:",b),s=!1}),r=(b,d,_)=>{if(da(d,e.directory))return;try{let g=i.get(d);if(!g){let k=a.prepare("SELECT id FROM resources WHERE path = ?").get(d);if(k)g=k.id,i.set(d,g);else{g=gn();let F=Date.now();a.prepare("INSERT INTO resources (id, path, type, created_at) VALUES (?, ?, ?, ?)").run(g,d,_,F),i.set(d,g)}}let h=gn(),w=Date.now();a.prepare("INSERT INTO events (id, type, resource_id, timestamp) VALUES (?, ?, ?, ?)").run(h,b,g,w)}catch(g){console.error("[events] Failed to record event:",g)}};return{recordEvent:(b,d,_)=>{if(s)c.push({type:b,path:d,resourceType:_});else r(b,d,_)},getAnalytics:async(b)=>{await o;let d=b||e.directory,g=a.prepare(`
|
|
756
756
|
SELECT r.path, COUNT(e.id) as views
|
|
757
757
|
FROM resources r
|
|
758
758
|
JOIN events e ON e.resource_id = r.id
|
|
@@ -792,7 +792,7 @@ Arguments:
|
|
|
792
792
|
path Directory or file to serve (default: current directory)
|
|
793
793
|
|
|
794
794
|
Commands:
|
|
795
|
-
docs View llmd documentation (clones repo to ~/.local/share/llmd)
|
|
795
|
+
docs View llmd documentation (clones repo to ~/.local/share/llmd-docs)
|
|
796
796
|
analytics [subcommand] Manage analytics
|
|
797
797
|
view [path] Open to analytics page (default subcommand)
|
|
798
798
|
enable Enable analytics tracking
|
|
@@ -821,10 +821,10 @@ Examples:
|
|
|
821
821
|
llmd analytics view ~/my-project # Open analytics for specific project
|
|
822
822
|
llmd analytics enable # Enable analytics tracking
|
|
823
823
|
llmd analytics disable # Disable analytics tracking
|
|
824
|
-
llmd --fonts modern # Use modern font combo (
|
|
824
|
+
llmd --fonts modern # Use modern font combo (Inter + JetBrains Mono)
|
|
825
825
|
llmd --theme nord # Use Nord color theme
|
|
826
826
|
llmd --theme dracula --watch # Dracula theme with live reload
|
|
827
|
-
`,vm=(e,n)=>{let t=e[n+1];if(t==="enable"||t==="disable"||t==="view")return{subcommand:t,nextIndex:n+1};return{subcommand:"view",nextIndex:n}},Cm=(e,n)=>{if(e==="--help"||e==="-h")return n.help=!0,!0;if(e==="--version")return n.version=!0,!0;if(e==="docs")return n.docs=!0,!0;if(e==="--open")return n.open=!0,!0;if(e==="--no-open")return n.open=!1,!0;if(e==="--watch")return n.watch=!0,!0;if(e==="--no-watch")return n.watch=!1,!0;return!1},jm=(e,n,t,a)=>{if(e==="--port")return a.port=Number.parseInt(n[t+1]??"0",10),t+1;if(e==="--theme")return a.theme=n[t+1],t+1;if(e==="--fonts")return a.fontTheme=n[t+1],t+1;if(e==="analytics"){a.analytics=!0;let{subcommand:i,nextIndex:c}=vm(n,t);return a.analyticsSubcommand=i,c}return t},Am=(e)=>{let n={},t;for(let a=0;a<e.length;a+=1){let i=e[a];if(!i)continue;if(Cm(i,n))continue;let c=jm(i,e,a,n);if(c!==a){a=c;continue}if(!i.startsWith("-"))t=i}return{path:t,flags:n}},qm=(e)=>{let n=wm(e)?e:xm(process.cwd(),e);if(n.endsWith(".md"))return{directory:ym(n),initialFile:n};return{directory:n}},zm=(e)=>{let{path:n=".",flags:t}=e,{directory:a,initialFile:i}=qm(n),c=vc();return{directory:a,initialFile:i,port:t.port??0,theme:t.theme??c.theme??"dark",fontTheme:t.fontTheme??c.fontTheme??"sans",open:t.open??!0,watch:t.watch??!1,openToAnalytics:t.analytics??!1}},Zm=(e)=>{if(!fm(e.directory))throw Error(`Directory not found: ${e.directory}`);if(!Fc(e.theme)){let n=Zc();throw Error(`Theme "${e.theme}" not found. Available themes: ${n.join(", ")}`)}if(!qc(e.fontTheme)){let n=Ac();throw Error(`Font "${e.fontTheme}" not found. Available fonts: ${n.join(", ")}`)}if(e.port<0||e.port>65535)throw Error(`Invalid port: ${e.port}. Must be 0-65535`)},Fm=()=>{console.log($m)},Em=()=>{console.log(`llmd v${km}`)},ka=(e)=>{let n=Am(e);if(n.flags.help)return Fm(),{type:"exit"};if(n.flags.version)return Em(),{type:"exit"};if(n.flags.docs)return{type:"docs"};if(n.flags.analytics&&n.flags.analyticsSubcommand){if(n.flags.analyticsSubcommand==="enable")return{type:"analytics-enable"};if(n.flags.analyticsSubcommand==="disable")return{type:"analytics-disable"}}let t=zm(n);return Zm(t),{type:"config",config:t}};import{existsSync as Um,mkdirSync as Wm}from"node:fs";import{homedir as Nm}from"node:os";import{basename as Of,dirname as If,join as $a}from"node:path";var va=(e)=>{
|
|
827
|
+
`,vm=(e,n)=>{let t=e[n+1];if(t==="enable"||t==="disable"||t==="view")return{subcommand:t,nextIndex:n+1};return{subcommand:"view",nextIndex:n}},Cm=(e,n)=>{if(e==="--help"||e==="-h")return n.help=!0,!0;if(e==="--version")return n.version=!0,!0;if(e==="docs")return n.docs=!0,!0;if(e==="--open")return n.open=!0,!0;if(e==="--no-open")return n.open=!1,!0;if(e==="--watch")return n.watch=!0,!0;if(e==="--no-watch")return n.watch=!1,!0;return!1},jm=(e,n,t,a)=>{if(e==="--port")return a.port=Number.parseInt(n[t+1]??"0",10),t+1;if(e==="--theme")return a.theme=n[t+1],t+1;if(e==="--fonts")return a.fontTheme=n[t+1],t+1;if(e==="analytics"){a.analytics=!0;let{subcommand:i,nextIndex:c}=vm(n,t);return a.analyticsSubcommand=i,c}return t},Am=(e)=>{let n={},t;for(let a=0;a<e.length;a+=1){let i=e[a];if(!i)continue;if(Cm(i,n))continue;let c=jm(i,e,a,n);if(c!==a){a=c;continue}if(!i.startsWith("-"))t=i}return{path:t,flags:n}},qm=(e)=>{let n=wm(e)?e:xm(process.cwd(),e);if(n.endsWith(".md"))return{directory:ym(n),initialFile:n};return{directory:n}},zm=(e)=>{let{path:n=".",flags:t}=e,{directory:a,initialFile:i}=qm(n),c=vc();return{directory:a,initialFile:i,port:t.port??0,theme:t.theme??c.theme??"dark",fontTheme:t.fontTheme??c.fontTheme??"sans",open:t.open??!0,watch:t.watch??!1,openToAnalytics:t.analytics??!1}},Zm=(e)=>{if(!fm(e.directory))throw Error(`Directory not found: ${e.directory}`);if(!Fc(e.theme)){let n=Zc();throw Error(`Theme "${e.theme}" not found. Available themes: ${n.join(", ")}`)}if(!qc(e.fontTheme)){let n=Ac();throw Error(`Font "${e.fontTheme}" not found. Available fonts: ${n.join(", ")}`)}if(e.port<0||e.port>65535)throw Error(`Invalid port: ${e.port}. Must be 0-65535`)},Fm=()=>{console.log($m)},Em=()=>{console.log(`llmd v${km}`)},ka=(e)=>{let n=Am(e);if(n.flags.help)return Fm(),{type:"exit"};if(n.flags.version)return Em(),{type:"exit"};if(n.flags.docs)return{type:"docs"};if(n.flags.analytics&&n.flags.analyticsSubcommand){if(n.flags.analyticsSubcommand==="enable")return{type:"analytics-enable"};if(n.flags.analyticsSubcommand==="disable")return{type:"analytics-disable"}}let t=zm(n);return Zm(t),{type:"config",config:t}};import{existsSync as Um,mkdirSync as Wm}from"node:fs";import{homedir as Nm}from"node:os";import{basename as Of,dirname as If,join as $a}from"node:path";var va=(e)=>{if(typeof Bun<"u"&&globalThis.Bun){let{Database:t}=Z("bun:sqlite");return new t(e)}return new(Z("libsql"))(e)},Ca=()=>{let n=process.env.XDG_DATA_HOME||$a(Nm(),".local","share"),t=$a(n,"llmd");if(!Um(t))Wm(t,{recursive:!0});return $a(t,"llmd.db")};var ja=(e)=>{e.exec("PRAGMA foreign_keys = ON"),e.exec(`
|
|
828
828
|
CREATE TABLE IF NOT EXISTS resources (
|
|
829
829
|
id TEXT PRIMARY KEY,
|
|
830
830
|
path TEXT UNIQUE NOT NULL,
|
|
@@ -1081,5 +1081,5 @@ ${Qh.map((n,t)=>`${Kh[t]}${n}\x1B[0m`).join(`
|
|
|
1081
1081
|
\u2717 Unknown error occurred
|
|
1082
1082
|
`);process.exit(1)}};sf();
|
|
1083
1083
|
|
|
1084
|
-
//# debugId=
|
|
1084
|
+
//# debugId=D674F036AF69C54464756E2164756E21
|
|
1085
1085
|
//# sourceMappingURL=llmd.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llmd",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Local markdown server for LLM-generated docs",
|
|
5
5
|
"author": "Phil Zona <phil.b.zona@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"scripts": {
|
|
32
32
|
"dev": "bun --hot index.ts",
|
|
33
33
|
"test": "bun test",
|
|
34
|
-
"build": "
|
|
34
|
+
"build": "bash scripts/build.sh",
|
|
35
35
|
"prepublishOnly": "bun test && bun run build",
|
|
36
36
|
"preview": "bun scripts/generate-preview.ts",
|
|
37
37
|
"check-contrast": "bun scripts/check-contrast.ts",
|
|
@@ -53,16 +53,13 @@
|
|
|
53
53
|
"@types/ws": "^8.18.1",
|
|
54
54
|
"fast-glob": "^3.3.3",
|
|
55
55
|
"figlet": "^1.9.4",
|
|
56
|
+
"libsql": "^0.5.22",
|
|
56
57
|
"marked": "^17.0.1",
|
|
57
58
|
"shiki": "^3.20.0",
|
|
58
59
|
"ws": "^8.18.3"
|
|
59
60
|
},
|
|
60
|
-
"optionalDependencies": {
|
|
61
|
-
"better-sqlite3": "^12.5.0"
|
|
62
|
-
},
|
|
63
61
|
"devDependencies": {
|
|
64
62
|
"@biomejs/biome": "2.3.8",
|
|
65
|
-
"@types/better-sqlite3": "^7.6.13",
|
|
66
63
|
"@types/bun": "latest",
|
|
67
64
|
"@types/figlet": "^1.7.0",
|
|
68
65
|
"husky": "^9.1.7",
|