@taciturnaxolotl/traverse 0.1.2 → 0.1.4

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
@@ -1,6 +1,9 @@
1
1
  # traverse
2
2
 
3
- [![the diagram view](https://l4.dunkirk.sh/i/eAqE3K4HnppN.webp)](https://traverse.dunkirk.sh/diagram/6121f05c-a5ef-4ecf-8ffc-02534c5e767c)
3
+ [![the diagram view](https://l4.dunkirk.sh/i/K1vXY48_1NcQ.webp)](https://traverse.dunkirk.sh/diagram/6121f05c-a5ef-4ecf-8ffc-02534c5e767c)
4
+
5
+ > [!NOTE]
6
+ > The canonical repo for this is hosted on tangled over at [`dunkirk.sh/traverse`](https://tangled.org/@dunkirk.sh/traverse)
4
7
 
5
8
  One of my favorite features about [amp](https://ampcode.com) is their walkthrough feature. It runs a sub agent which goes and breaks your repo into parts and then sends it up to amp's services to get rendered into a nice web page! I got curious and ended up dumping the tool prompt for both the walkthrough subagent and the tool prompt that generates the diagram.
6
9
 
@@ -43,8 +46,6 @@ For other agents its the same JSON config typically.
43
46
 
44
47
  ## Config
45
48
 
46
- ### json config
47
-
48
49
  On macos edit/create `~/Library/Application Support/traverse/config.json`. If you are on Linux then `~/.config/traverse/config.json` (or `$XDG_CONFIG_HOME/traverse/config.json`)
49
50
 
50
51
  ```json
@@ -55,7 +56,7 @@ On macos edit/create `~/Library/Application Support/traverse/config.json`. If yo
55
56
  }
56
57
  ```
57
58
 
58
- ### env vars
59
+ alteratively or suplementally you can use env vars to define the same options:
59
60
 
60
61
  | var | default | description |
61
62
  | -------------------- | ----------------------------- | ------------------------------------------ |
@@ -64,8 +65,6 @@ On macos edit/create `~/Library/Application Support/traverse/config.json`. If yo
64
65
  | `TRAVERSE_SHARE_URL` | `https://traverse.dunkirk.sh` | share server url |
65
66
  | `TRAVERSE_DATA_DIR` | platform default | sqlite db location |
66
67
 
67
- The canonical repo for this is hosted on tangled over at [`dunkirk.sh/traverse`](https://tangled.org/@dunkirk.sh/traverse)
68
-
69
68
  <p align="center">
70
69
  <img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/main/.github/images/line-break.svg" />
71
70
  </p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taciturnaxolotl/traverse",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Interactive code walkthrough diagrams via MCP",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -10,6 +10,7 @@
10
10
  "files": [
11
11
  "src",
12
12
  "bin",
13
+ "fonts",
13
14
  "icon.svg"
14
15
  ],
15
16
  "scripts": {
@@ -23,7 +24,6 @@
23
24
  "typescript": "^5"
24
25
  },
25
26
  "dependencies": {
26
- "@fontsource/inter": "^5.2.8",
27
27
  "@modelcontextprotocol/sdk": "^1.26.0",
28
28
  "@resvg/resvg-wasm": "^2.6.2",
29
29
  "satori": "^0.19.1"
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ import { z } from "zod/v4";
4
4
  import { generateViewerHTML } from "./template.ts";
5
5
  import type { WalkthroughDiagram } from "./types.ts";
6
6
  import { initDb, loadAllDiagrams, saveDiagram, deleteDiagramFromDb, generateId, getSharedUrl, saveSharedUrl } from "./storage.ts";
7
- import { generateOgImage } from "./og.ts";
7
+ import { generateOgImage, generateIndexOgImage } from "./og.ts";
8
8
  import { loadConfig } from "./config.ts";
9
9
 
10
10
  const config = loadConfig();
@@ -153,11 +153,22 @@ try {
153
153
  });
154
154
  }
155
155
 
156
+ // Index OG image
157
+ if (url.pathname === "/og.png") {
158
+ const png = await generateIndexOgImage(MODE, diagrams.size);
159
+ return new Response(png, {
160
+ headers: {
161
+ "Content-Type": "image/png",
162
+ "Cache-Control": "public, max-age=3600",
163
+ },
164
+ });
165
+ }
166
+
156
167
  // List available diagrams
157
168
  if (url.pathname === "/") {
158
169
  const html = MODE === "server"
159
- ? generateServerIndexHTML(diagrams.size, VERSION)
160
- : generateLocalIndexHTML(diagrams, VERSION);
170
+ ? generateServerIndexHTML(diagrams.size, VERSION, url.origin)
171
+ : generateLocalIndexHTML(diagrams, VERSION, url.origin);
161
172
  return new Response(html, {
162
173
  headers: { "Content-Type": "text/html; charset=utf-8" },
163
174
  });
@@ -305,7 +316,7 @@ function generate404HTML(title: string, message: string): string {
305
316
  </html>`;
306
317
  }
307
318
 
308
- function generateLocalIndexHTML(diagrams: Map<string, WalkthroughDiagram>, gitHash: string): string {
319
+ function generateLocalIndexHTML(diagrams: Map<string, WalkthroughDiagram>, gitHash: string, baseUrl: string): string {
309
320
  const items = [...diagrams.entries()]
310
321
  .map(
311
322
  ([id, d]) => {
@@ -346,6 +357,7 @@ function generateLocalIndexHTML(diagrams: Map<string, WalkthroughDiagram>, gitHa
346
357
  </div>`
347
358
  : `<div class="diagram-list">${items}</div>`;
348
359
 
360
+ const diagramCount = diagrams.size;
349
361
  return `<!DOCTYPE html>
350
362
  <html lang="en">
351
363
  <head>
@@ -353,6 +365,14 @@ function generateLocalIndexHTML(diagrams: Map<string, WalkthroughDiagram>, gitHa
353
365
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
354
366
  <title>Traverse</title>
355
367
  <link rel="icon" href="/icon.svg" type="image/svg+xml" />
368
+ <meta property="og:type" content="website" />
369
+ <meta property="og:title" content="Traverse" />
370
+ <meta property="og:description" content="Interactive code walkthrough diagrams. ${diagramCount} diagram${diagramCount !== 1 ? "s" : ""}." />
371
+ <meta property="og:image" content="${escapeHTML(baseUrl)}/og.png" />
372
+ <meta name="twitter:card" content="summary_large_image" />
373
+ <meta name="twitter:title" content="Traverse" />
374
+ <meta name="twitter:description" content="Interactive code walkthrough diagrams." />
375
+ <meta name="twitter:image" content="${escapeHTML(baseUrl)}/og.png" />
356
376
  <style>
357
377
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
358
378
  :root {
@@ -511,7 +531,7 @@ function generateLocalIndexHTML(diagrams: Map<string, WalkthroughDiagram>, gitHa
511
531
  </html>`;
512
532
  }
513
533
 
514
- function generateServerIndexHTML(diagramCount: number, gitHash: string): string {
534
+ function generateServerIndexHTML(diagramCount: number, gitHash: string, baseUrl: string): string {
515
535
  return `<!DOCTYPE html>
516
536
  <html lang="en">
517
537
  <head>
@@ -519,6 +539,14 @@ function generateServerIndexHTML(diagramCount: number, gitHash: string): string
519
539
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
520
540
  <title>Traverse</title>
521
541
  <link rel="icon" href="/icon.svg" type="image/svg+xml" />
542
+ <meta property="og:type" content="website" />
543
+ <meta property="og:title" content="Traverse" />
544
+ <meta property="og:description" content="Interactive code walkthrough diagrams, shareable with anyone. ${diagramCount} diagram${diagramCount !== 1 ? "s" : ""} shared." />
545
+ <meta property="og:image" content="${escapeHTML(baseUrl)}/og.png" />
546
+ <meta name="twitter:card" content="summary_large_image" />
547
+ <meta name="twitter:title" content="Traverse" />
548
+ <meta name="twitter:description" content="Interactive code walkthrough diagrams, shareable with anyone." />
549
+ <meta name="twitter:image" content="${escapeHTML(baseUrl)}/og.png" />
522
550
  <style>
523
551
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
524
552
  :root {
package/src/og.ts CHANGED
@@ -3,7 +3,7 @@ import { initWasm, Resvg } from "@resvg/resvg-wasm";
3
3
  import { join } from "path";
4
4
 
5
5
  // Load Inter font files (woff, not woff2 — satori doesn't support woff2)
6
- const fontsDir = join(import.meta.dir, "../node_modules/@fontsource/inter/files");
6
+ const fontsDir = join(import.meta.dir, "../fonts");
7
7
  const [interRegular, interBold] = await Promise.all([
8
8
  Bun.file(join(fontsDir, "inter-latin-400-normal.woff")).arrayBuffer(),
9
9
  Bun.file(join(fontsDir, "inter-latin-700-normal.woff")).arrayBuffer(),
@@ -16,6 +16,114 @@ await initWasm(Bun.file(wasmPath).arrayBuffer());
16
16
  // Cache generated images by diagram ID
17
17
  const cache = new Map<string, Buffer>();
18
18
 
19
+ export async function generateIndexOgImage(
20
+ mode: "local" | "server",
21
+ diagramCount: number,
22
+ ): Promise<Buffer> {
23
+ const cacheKey = `__index_${mode}_${diagramCount}`;
24
+ const cached = cache.get(cacheKey);
25
+ if (cached) return cached;
26
+
27
+ const subtitle = mode === "server"
28
+ ? `${diagramCount} diagram${diagramCount !== 1 ? "s" : ""} shared`
29
+ : `${diagramCount} diagram${diagramCount !== 1 ? "s" : ""}`;
30
+
31
+ const tagline = mode === "server"
32
+ ? "Interactive code walkthrough diagrams, shareable with anyone."
33
+ : "Interactive code walkthrough diagrams";
34
+
35
+ const svg = await satori(
36
+ {
37
+ type: "div",
38
+ props: {
39
+ style: {
40
+ width: "100%",
41
+ height: "100%",
42
+ display: "flex",
43
+ flexDirection: "column",
44
+ justifyContent: "center",
45
+ alignItems: "center",
46
+ padding: "60px",
47
+ backgroundColor: "#0a0a0a",
48
+ color: "#e5e5e5",
49
+ fontFamily: "Inter",
50
+ },
51
+ children: [
52
+ {
53
+ type: "div",
54
+ props: {
55
+ style: {
56
+ display: "flex",
57
+ flexDirection: "column",
58
+ alignItems: "center",
59
+ gap: "20px",
60
+ },
61
+ children: [
62
+ {
63
+ type: "div",
64
+ props: {
65
+ style: {
66
+ fontSize: "80px",
67
+ fontWeight: 700,
68
+ color: "#e5e5e5",
69
+ },
70
+ children: "Traverse",
71
+ },
72
+ },
73
+ {
74
+ type: "div",
75
+ props: {
76
+ style: {
77
+ fontSize: "32px",
78
+ color: "#a3a3a3",
79
+ textAlign: "center",
80
+ },
81
+ children: tagline,
82
+ },
83
+ },
84
+ {
85
+ type: "div",
86
+ props: {
87
+ style: {
88
+ display: "flex",
89
+ alignItems: "center",
90
+ gap: "8px",
91
+ marginTop: "16px",
92
+ fontSize: "24px",
93
+ color: "#a3a3a3",
94
+ backgroundColor: "#1c1c1e",
95
+ padding: "10px 24px",
96
+ borderRadius: "8px",
97
+ border: "1px solid #262626",
98
+ },
99
+ children: subtitle,
100
+ },
101
+ },
102
+ ],
103
+ },
104
+ },
105
+ ],
106
+ },
107
+ },
108
+ {
109
+ width: 1200,
110
+ height: 630,
111
+ fonts: [
112
+ { name: "Inter", data: interRegular, weight: 400, style: "normal" as const },
113
+ { name: "Inter", data: interBold, weight: 700, style: "normal" as const },
114
+ ],
115
+ },
116
+ );
117
+
118
+ const resvg = new Resvg(svg, {
119
+ fitTo: { mode: "width", value: 1200 },
120
+ });
121
+ const png = Buffer.from(resvg.render().asPng());
122
+
123
+ cache.set(cacheKey, png);
124
+ return png;
125
+ }
126
+
19
127
  export async function generateOgImage(
20
128
  id: string,
21
129
  summary: string,