snatching amp's walkthrough for my own purposes mwhahaha traverse.dunkirk.sh/diagram/6121f05c-a5ef-4ecf-8ffc-02534c5e767c

Add MCP server, template viewer, and type definitions

Traverse MCP server with interactive Mermaid diagram viewer.
Fixes highlight.js CDN loading (use browser bundle instead of
CommonJS modules), responsive layout with sidebar-to-bottom
breakpoint at 1300px, and viewport-aware SVG scaling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

dunkirk.sh 2783670b 90e17ff0

verified
+1010
+34
.gitignore
··· 1 + # dependencies (bun install) 2 + node_modules 3 + 4 + # output 5 + out 6 + dist 7 + *.tgz 8 + 9 + # code coverage 10 + coverage 11 + *.lcov 12 + 13 + # logs 14 + logs 15 + _.log 16 + report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 + 18 + # dotenv environment variable files 19 + .env 20 + .env.development.local 21 + .env.test.local 22 + .env.production.local 23 + .env.local 24 + 25 + # caches 26 + .eslintcache 27 + .cache 28 + *.tsbuildinfo 29 + 30 + # IntelliJ based IDEs 31 + .idea 32 + 33 + # Finder (MacOS) folder config 34 + .DS_Store
+106
CLAUDE.md
··· 1 + 2 + Default to using Bun instead of Node.js. 3 + 4 + - Use `bun <file>` instead of `node <file>` or `ts-node <file>` 5 + - Use `bun test` instead of `jest` or `vitest` 6 + - Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild` 7 + - Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` 8 + - Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>` 9 + - Use `bunx <package> <command>` instead of `npx <package> <command>` 10 + - Bun automatically loads .env, so don't use dotenv. 11 + 12 + ## APIs 13 + 14 + - `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`. 15 + - `bun:sqlite` for SQLite. Don't use `better-sqlite3`. 16 + - `Bun.redis` for Redis. Don't use `ioredis`. 17 + - `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`. 18 + - `WebSocket` is built-in. Don't use `ws`. 19 + - Prefer `Bun.file` over `node:fs`'s readFile/writeFile 20 + - Bun.$`ls` instead of execa. 21 + 22 + ## Testing 23 + 24 + Use `bun test` to run tests. 25 + 26 + ```ts#index.test.ts 27 + import { test, expect } from "bun:test"; 28 + 29 + test("hello world", () => { 30 + expect(1).toBe(1); 31 + }); 32 + ``` 33 + 34 + ## Frontend 35 + 36 + Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind. 37 + 38 + Server: 39 + 40 + ```ts#index.ts 41 + import index from "./index.html" 42 + 43 + Bun.serve({ 44 + routes: { 45 + "/": index, 46 + "/api/users/:id": { 47 + GET: (req) => { 48 + return new Response(JSON.stringify({ id: req.params.id })); 49 + }, 50 + }, 51 + }, 52 + // optional websocket support 53 + websocket: { 54 + open: (ws) => { 55 + ws.send("Hello, world!"); 56 + }, 57 + message: (ws, message) => { 58 + ws.send(message); 59 + }, 60 + close: (ws) => { 61 + // handle close 62 + } 63 + }, 64 + development: { 65 + hmr: true, 66 + console: true, 67 + } 68 + }) 69 + ``` 70 + 71 + HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle. 72 + 73 + ```html#index.html 74 + <html> 75 + <body> 76 + <h1>Hello, world!</h1> 77 + <script type="module" src="./frontend.tsx"></script> 78 + </body> 79 + </html> 80 + ``` 81 + 82 + With the following `frontend.tsx`: 83 + 84 + ```tsx#frontend.tsx 85 + import React from "react"; 86 + import { createRoot } from "react-dom/client"; 87 + 88 + // import .css files directly and it works 89 + import './index.css'; 90 + 91 + const root = createRoot(document.body); 92 + 93 + export default function Frontend() { 94 + return <h1>Hello, world!</h1>; 95 + } 96 + 97 + root.render(<Frontend />); 98 + ``` 99 + 100 + Then, run index.ts 101 + 102 + ```sh 103 + bun --hot ./index.ts 104 + ``` 105 + 106 + For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
+211
bun.lock
··· 1 + { 2 + "lockfileVersion": 1, 3 + "configVersion": 1, 4 + "workspaces": { 5 + "": { 6 + "name": "traverse", 7 + "dependencies": { 8 + "@modelcontextprotocol/sdk": "^1.26.0", 9 + }, 10 + "devDependencies": { 11 + "@types/bun": "latest", 12 + }, 13 + "peerDependencies": { 14 + "typescript": "^5", 15 + }, 16 + }, 17 + }, 18 + "packages": { 19 + "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], 20 + 21 + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.26.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg=="], 22 + 23 + "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], 24 + 25 + "@types/node": ["@types/node@25.2.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ=="], 26 + 27 + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], 28 + 29 + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], 30 + 31 + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], 32 + 33 + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], 34 + 35 + "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], 36 + 37 + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], 38 + 39 + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], 40 + 41 + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], 42 + 43 + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], 44 + 45 + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], 46 + 47 + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], 48 + 49 + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], 50 + 51 + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], 52 + 53 + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], 54 + 55 + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 56 + 57 + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], 58 + 59 + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], 60 + 61 + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], 62 + 63 + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], 64 + 65 + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], 66 + 67 + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], 68 + 69 + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], 70 + 71 + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], 72 + 73 + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], 74 + 75 + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], 76 + 77 + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], 78 + 79 + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], 80 + 81 + "express-rate-limit": ["express-rate-limit@8.2.1", "", { "dependencies": { "ip-address": "10.0.1" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g=="], 82 + 83 + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], 84 + 85 + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], 86 + 87 + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], 88 + 89 + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], 90 + 91 + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], 92 + 93 + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], 94 + 95 + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], 96 + 97 + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], 98 + 99 + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], 100 + 101 + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], 102 + 103 + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], 104 + 105 + "hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="], 106 + 107 + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], 108 + 109 + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], 110 + 111 + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], 112 + 113 + "ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="], 114 + 115 + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], 116 + 117 + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], 118 + 119 + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], 120 + 121 + "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], 122 + 123 + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], 124 + 125 + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], 126 + 127 + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], 128 + 129 + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], 130 + 131 + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], 132 + 133 + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], 134 + 135 + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], 136 + 137 + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 138 + 139 + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], 140 + 141 + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], 142 + 143 + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], 144 + 145 + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], 146 + 147 + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], 148 + 149 + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], 150 + 151 + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], 152 + 153 + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], 154 + 155 + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], 156 + 157 + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], 158 + 159 + "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], 160 + 161 + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], 162 + 163 + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], 164 + 165 + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], 166 + 167 + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], 168 + 169 + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], 170 + 171 + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], 172 + 173 + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], 174 + 175 + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], 176 + 177 + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], 178 + 179 + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], 180 + 181 + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], 182 + 183 + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], 184 + 185 + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], 186 + 187 + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], 188 + 189 + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], 190 + 191 + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], 192 + 193 + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], 194 + 195 + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 196 + 197 + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 198 + 199 + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], 200 + 201 + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], 202 + 203 + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 204 + 205 + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], 206 + 207 + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], 208 + 209 + "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], 210 + } 211 + }
+20
package.json
··· 1 + { 2 + "name": "traverse", 3 + "version": "0.1.0", 4 + "module": "src/index.ts", 5 + "type": "module", 6 + "private": true, 7 + "scripts": { 8 + "start": "bun run src/index.ts", 9 + "dev": "bun --hot run src/index.ts" 10 + }, 11 + "devDependencies": { 12 + "@types/bun": "latest" 13 + }, 14 + "peerDependencies": { 15 + "typescript": "^5" 16 + }, 17 + "dependencies": { 18 + "@modelcontextprotocol/sdk": "^1.26.0" 19 + } 20 + }
+127
src/index.ts
··· 1 + import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 + import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 + import { z } from "zod/v4"; 4 + import { generateViewerHTML } from "./template.ts"; 5 + import type { WalkthroughDiagram } from "./types.ts"; 6 + 7 + const PORT = parseInt(process.env.TRAVERSE_PORT || "4173", 10); 8 + 9 + // In-memory diagram store 10 + const diagrams = new Map<string, WalkthroughDiagram>(); 11 + let diagramCounter = 0; 12 + 13 + // --- Web server for serving interactive diagrams --- 14 + Bun.serve({ 15 + port: PORT, 16 + fetch(req) { 17 + const url = new URL(req.url); 18 + const match = url.pathname.match(/^\/diagram\/(\w+)$/); 19 + 20 + if (match) { 21 + const id = match[1]!; 22 + const diagram = diagrams.get(id); 23 + if (!diagram) { 24 + return new Response("Diagram not found", { status: 404 }); 25 + } 26 + return new Response(generateViewerHTML(diagram), { 27 + headers: { "Content-Type": "text/html; charset=utf-8" }, 28 + }); 29 + } 30 + 31 + // List available diagrams 32 + if (url.pathname === "/") { 33 + if (diagrams.size === 0) { 34 + return new Response( 35 + "<html><body style='font-family:system-ui;padding:40px;color:#666'><h2>Traverse</h2><p>No diagrams yet. Use the MCP tool to create one.</p></body></html>", 36 + { headers: { "Content-Type": "text/html" } }, 37 + ); 38 + } 39 + const links = [...diagrams.entries()] 40 + .map( 41 + ([id, d]) => 42 + `<li><a href="/diagram/${id}">${escapeHTML(d.summary)}</a></li>`, 43 + ) 44 + .join("\n"); 45 + return new Response( 46 + `<html><body style='font-family:system-ui;padding:40px'><h2>Traverse</h2><ul>${links}</ul></body></html>`, 47 + { headers: { "Content-Type": "text/html" } }, 48 + ); 49 + } 50 + 51 + return new Response("Not found", { status: 404 }); 52 + }, 53 + }); 54 + 55 + // --- MCP Server --- 56 + const server = new McpServer({ 57 + name: "traverse", 58 + version: "0.1.0", 59 + }); 60 + 61 + const nodeMetadataSchema = z.object({ 62 + title: z.string().describe("Display name for the node"), 63 + description: z 64 + .string() 65 + .describe("Detailed markdown explanation of this component"), 66 + links: z 67 + .array( 68 + z.object({ 69 + label: z.string().describe("Display text, e.g. src/auth.ts"), 70 + url: z.string().describe("File path or URL"), 71 + }), 72 + ) 73 + .optional() 74 + .describe("Related files or documentation"), 75 + codeSnippet: z.string().optional().describe("Optional code example"), 76 + threadID: z 77 + .string() 78 + .optional() 79 + .describe("Optional link to deeper exploration thread"), 80 + }); 81 + 82 + server.registerTool("walkthrough_diagram", { 83 + title: "Walkthrough Diagram", 84 + description: 85 + "Render an interactive Mermaid diagram where users can click nodes to see detailed information about each component. Use plain text labels in Mermaid code, no HTML tags or custom styling.", 86 + inputSchema: z.object({ 87 + code: z 88 + .string() 89 + .describe( 90 + "Mermaid diagram code. Use plain text labels, no HTML tags. No custom styling/colors.", 91 + ), 92 + summary: z 93 + .string() 94 + .describe("One-sentence summary of what the diagram illustrates"), 95 + nodes: z 96 + .record(z.string(), nodeMetadataSchema) 97 + .describe( 98 + "Metadata for clickable nodes, keyed by node ID from mermaid code", 99 + ), 100 + }), 101 + }, async ({ code, summary, nodes }) => { 102 + const id = String(++diagramCounter); 103 + const diagram: WalkthroughDiagram = { code, summary, nodes }; 104 + diagrams.set(id, diagram); 105 + 106 + const url = `http://localhost:${PORT}/diagram/${id}`; 107 + 108 + return { 109 + content: [ 110 + { 111 + type: "text", 112 + text: `Interactive diagram ready.\n\nOpen in browser: ${url}\n\nClick nodes in the diagram to explore details about each component.`, 113 + }, 114 + ], 115 + }; 116 + }); 117 + 118 + // Connect MCP server to stdio transport 119 + const transport = new StdioServerTransport(); 120 + await server.connect(transport); 121 + 122 + function escapeHTML(str: string): string { 123 + return str 124 + .replace(/&/g, "&amp;") 125 + .replace(/</g, "&lt;") 126 + .replace(/>/g, "&gt;"); 127 + }
+465
src/template.ts
··· 1 + import type { WalkthroughDiagram } from "./types.ts"; 2 + 3 + export function generateViewerHTML(diagram: WalkthroughDiagram): string { 4 + const diagramJSON = JSON.stringify(diagram); 5 + 6 + return `<!DOCTYPE html> 7 + <html lang="en"> 8 + <head> 9 + <meta charset="UTF-8" /> 10 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 11 + <title>Traverse — ${escapeHTML(diagram.summary)}</title> 12 + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11/styles/github-dark.min.css" id="hljs-dark" disabled /> 13 + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11/styles/github.min.css" id="hljs-light" /> 14 + <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/highlight.min.js"></script> 15 + <script src="https://cdn.jsdelivr.net/npm/marked@15/marked.min.js"></script> 16 + <style> 17 + *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 18 + 19 + :root { 20 + --bg: #fafafa; 21 + --bg-panel: #ffffff; 22 + --border: #e2e2e2; 23 + --text: #1a1a1a; 24 + --text-muted: #666; 25 + --accent: #2563eb; 26 + --accent-hover: #1d4ed8; 27 + --node-hover: rgba(37, 99, 235, 0.08); 28 + --code-bg: #f4f4f5; 29 + --summary-bg: #f0f4ff; 30 + --shadow: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04); 31 + --shadow-lg: 0 4px 12px rgba(0,0,0,0.08), 0 2px 4px rgba(0,0,0,0.04); 32 + } 33 + 34 + @media (prefers-color-scheme: dark) { 35 + :root { 36 + --bg: #0a0a0a; 37 + --bg-panel: #141414; 38 + --border: #262626; 39 + --text: #e5e5e5; 40 + --text-muted: #a3a3a3; 41 + --accent: #3b82f6; 42 + --accent-hover: #60a5fa; 43 + --node-hover: rgba(59, 130, 246, 0.12); 44 + --code-bg: #1c1c1e; 45 + --summary-bg: #111827; 46 + --shadow: 0 1px 3px rgba(0,0,0,0.3); 47 + --shadow-lg: 0 4px 12px rgba(0,0,0,0.4); 48 + } 49 + } 50 + 51 + body { 52 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 53 + background: var(--bg); 54 + color: var(--text); 55 + height: 100vh; 56 + display: flex; 57 + flex-direction: column; 58 + overflow: hidden; 59 + } 60 + 61 + .summary-bar { 62 + padding: 12px 20px; 63 + background: var(--summary-bg); 64 + border-bottom: 1px solid var(--border); 65 + font-size: 14px; 66 + color: var(--text-muted); 67 + flex-shrink: 0; 68 + display: flex; 69 + align-items: center; 70 + gap: 8px; 71 + } 72 + 73 + .summary-bar .label { 74 + font-weight: 600; 75 + color: var(--accent); 76 + text-transform: uppercase; 77 + font-size: 11px; 78 + letter-spacing: 0.05em; 79 + } 80 + 81 + .main { 82 + display: flex; 83 + flex: 1; 84 + overflow: hidden; 85 + } 86 + 87 + .diagram-container { 88 + flex: 1; 89 + overflow: auto; 90 + display: flex; 91 + align-items: center; 92 + justify-content: center; 93 + padding: 32px; 94 + min-width: 0; 95 + } 96 + 97 + .diagram-container pre.mermaid { 98 + width: 100%; 99 + } 100 + 101 + .diagram-container svg { 102 + width: 100%; 103 + height: auto; 104 + } 105 + 106 + /* Make clickable nodes interactive */ 107 + .diagram-container .node { cursor: pointer; } 108 + .diagram-container .node:hover rect, 109 + .diagram-container .node:hover polygon, 110 + .diagram-container .node:hover circle, 111 + .diagram-container .node:hover .basic { 112 + filter: brightness(0.92); 113 + } 114 + 115 + .node.selected rect, 116 + .node.selected polygon, 117 + .node.selected circle, 118 + .node.selected .basic { 119 + stroke: var(--accent) !important; 120 + stroke-width: 2.5px !important; 121 + } 122 + 123 + .detail-panel { 124 + width: 420px; 125 + flex-shrink: 0; 126 + border-left: 1px solid var(--border); 127 + background: var(--bg-panel); 128 + display: none; 129 + flex-direction: column; 130 + overflow: hidden; 131 + } 132 + 133 + .detail-panel.open { 134 + display: flex; 135 + } 136 + 137 + @media (max-width: 1300px) { 138 + .main { 139 + flex-direction: column; 140 + } 141 + 142 + .detail-panel { 143 + width: 100%; 144 + border-left: none; 145 + border-top: 1px solid var(--border); 146 + max-height: 50vh; 147 + } 148 + } 149 + 150 + .detail-header { 151 + padding: 16px 20px; 152 + border-bottom: 1px solid var(--border); 153 + display: flex; 154 + align-items: center; 155 + justify-content: space-between; 156 + flex-shrink: 0; 157 + } 158 + 159 + .detail-header h2 { 160 + font-size: 16px; 161 + font-weight: 600; 162 + } 163 + 164 + .close-btn { 165 + background: none; 166 + border: none; 167 + color: var(--text-muted); 168 + cursor: pointer; 169 + padding: 4px; 170 + border-radius: 4px; 171 + font-size: 18px; 172 + line-height: 1; 173 + transition: color 0.15s; 174 + } 175 + 176 + .close-btn:hover { color: var(--text); } 177 + 178 + .detail-body { 179 + flex: 1; 180 + overflow-y: auto; 181 + padding: 20px; 182 + } 183 + 184 + .detail-body .description { 185 + font-size: 14px; 186 + line-height: 1.65; 187 + } 188 + 189 + .detail-body .description h1, 190 + .detail-body .description h2, 191 + .detail-body .description h3 { 192 + margin-top: 16px; 193 + margin-bottom: 8px; 194 + } 195 + 196 + .detail-body .description h1 { font-size: 18px; } 197 + .detail-body .description h2 { font-size: 16px; } 198 + .detail-body .description h3 { font-size: 14px; } 199 + 200 + .detail-body .description p { margin-bottom: 12px; } 201 + 202 + .detail-body .description code { 203 + background: var(--code-bg); 204 + padding: 2px 5px; 205 + border-radius: 3px; 206 + font-size: 13px; 207 + } 208 + 209 + .detail-body .description pre { 210 + background: var(--code-bg); 211 + padding: 12px; 212 + border-radius: 6px; 213 + overflow-x: auto; 214 + margin-bottom: 12px; 215 + } 216 + 217 + .detail-body .description pre code { 218 + background: none; 219 + padding: 0; 220 + } 221 + 222 + .detail-body .description ul, 223 + .detail-body .description ol { 224 + margin-bottom: 12px; 225 + padding-left: 20px; 226 + } 227 + 228 + .detail-body .description li { margin-bottom: 4px; } 229 + 230 + .section-label { 231 + font-size: 11px; 232 + font-weight: 600; 233 + text-transform: uppercase; 234 + letter-spacing: 0.05em; 235 + color: var(--text-muted); 236 + margin-top: 20px; 237 + margin-bottom: 8px; 238 + } 239 + 240 + .links-list { 241 + list-style: none; 242 + display: flex; 243 + flex-direction: column; 244 + gap: 4px; 245 + } 246 + 247 + .links-list a { 248 + color: var(--accent); 249 + text-decoration: none; 250 + font-size: 13px; 251 + font-family: "SF Mono", "Fira Code", monospace; 252 + padding: 4px 0; 253 + transition: color 0.15s; 254 + } 255 + 256 + .links-list a:hover { color: var(--accent-hover); text-decoration: underline; } 257 + 258 + .code-snippet { 259 + margin-top: 12px; 260 + } 261 + 262 + .code-snippet pre { 263 + background: var(--code-bg); 264 + border-radius: 6px; 265 + padding: 12px; 266 + overflow-x: auto; 267 + font-size: 13px; 268 + line-height: 1.5; 269 + } 270 + 271 + /* empty state */ 272 + .empty-hint { 273 + color: var(--text-muted); 274 + font-size: 13px; 275 + text-align: center; 276 + padding: 40px 20px; 277 + } 278 + </style> 279 + </head> 280 + <body> 281 + <div class="summary-bar"> 282 + <span class="label">Traverse</span> 283 + <span>${escapeHTML(diagram.summary)}</span> 284 + </div> 285 + 286 + <div class="main"> 287 + <div class="diagram-container"> 288 + <pre class="mermaid">${escapeHTML(diagram.code)}</pre> 289 + </div> 290 + 291 + <div class="detail-panel" id="detail-panel"> 292 + <div class="detail-header"> 293 + <h2 id="detail-title">Select a node</h2> 294 + <button class="close-btn" id="close-btn" aria-label="Close panel">&times;</button> 295 + </div> 296 + <div class="detail-body" id="detail-body"> 297 + <div class="empty-hint">Click a node in the diagram to view details.</div> 298 + </div> 299 + </div> 300 + </div> 301 + 302 + <script type="module"> 303 + import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs"; 304 + 305 + const DIAGRAM_DATA = ${diagramJSON}; 306 + 307 + function initTheme() { 308 + const dark = window.matchMedia("(prefers-color-scheme: dark)").matches; 309 + document.getElementById("hljs-dark").disabled = !dark; 310 + document.getElementById("hljs-light").disabled = dark; 311 + } 312 + initTheme(); 313 + window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", initTheme); 314 + 315 + async function init() { 316 + const dark = window.matchMedia("(prefers-color-scheme: dark)").matches; 317 + await mermaid.initialize({ 318 + startOnLoad: true, 319 + theme: dark ? "dark" : "default", 320 + flowchart: { useMaxWidth: false, htmlLabels: true, curve: "basis" }, 321 + securityLevel: "loose", 322 + }); 323 + 324 + // Wait for mermaid to finish rendering 325 + await mermaid.run(); 326 + 327 + // Set viewBox so the SVG scales to fit the container 328 + requestAnimationFrame(() => { 329 + fitDiagram(); 330 + attachClickHandlers(); 331 + }); 332 + 333 + window.addEventListener("resize", fitDiagram); 334 + } 335 + 336 + function fitDiagram() { 337 + const svg = document.querySelector(".diagram-container svg"); 338 + if (!svg) return; 339 + 340 + // Read the intrinsic size mermaid rendered 341 + const bbox = svg.getBBox(); 342 + const pad = 20; 343 + const vb = \`\${bbox.x - pad} \${bbox.y - pad} \${bbox.width + pad * 2} \${bbox.height + pad * 2}\`; 344 + svg.setAttribute("viewBox", vb); 345 + svg.removeAttribute("width"); 346 + svg.removeAttribute("height"); 347 + } 348 + 349 + function attachClickHandlers() { 350 + const svg = document.querySelector(".diagram-container svg"); 351 + if (!svg) return; 352 + 353 + const nodeIds = Object.keys(DIAGRAM_DATA.nodes); 354 + 355 + // Find all node groups in the SVG 356 + const allNodes = svg.querySelectorAll(".node"); 357 + 358 + allNodes.forEach(nodeEl => { 359 + // Extract node ID from the element 360 + const id = nodeEl.id; 361 + if (!id) return; 362 + 363 + // Match against our known node IDs 364 + const matchedId = nodeIds.find(nid => 365 + id === nid || 366 + id.endsWith("-" + nid) || 367 + id.startsWith("flowchart-" + nid + "-") || 368 + id.includes("-" + nid + "-") 369 + ); 370 + 371 + if (matchedId) { 372 + nodeEl.style.cursor = "pointer"; 373 + nodeEl.dataset.nodeId = matchedId; 374 + nodeEl.addEventListener("click", (e) => { 375 + e.stopPropagation(); 376 + selectNode(matchedId, nodeEl); 377 + }); 378 + } 379 + }); 380 + 381 + // Click outside to deselect 382 + document.addEventListener("click", (e) => { 383 + if (!e.target.closest(".detail-panel") && !e.target.closest(".node")) { 384 + deselectAll(); 385 + } 386 + }); 387 + } 388 + 389 + let selectedEl = null; 390 + 391 + function selectNode(nodeId, el) { 392 + const meta = DIAGRAM_DATA.nodes[nodeId]; 393 + if (!meta) return; 394 + 395 + // Update selection styling 396 + if (selectedEl) selectedEl.classList.remove("selected"); 397 + el.classList.add("selected"); 398 + selectedEl = el; 399 + 400 + // Update panel 401 + const panel = document.getElementById("detail-panel"); 402 + const title = document.getElementById("detail-title"); 403 + const body = document.getElementById("detail-body"); 404 + 405 + title.textContent = meta.title; 406 + 407 + let html = '<div class="description">' + marked.parse(meta.description) + "</div>"; 408 + 409 + if (meta.links && meta.links.length > 0) { 410 + html += '<div class="section-label">Related Files</div>'; 411 + html += '<ul class="links-list">'; 412 + meta.links.forEach(link => { 413 + html += '<li><a href="' + escapeAttr(link.url) + '">' + escapeText(link.label) + "</a></li>"; 414 + }); 415 + html += "</ul>"; 416 + } 417 + 418 + if (meta.codeSnippet) { 419 + html += '<div class="section-label">Code</div>'; 420 + html += '<div class="code-snippet"><pre><code>' + escapeText(meta.codeSnippet) + "</code></pre></div>"; 421 + } 422 + 423 + body.innerHTML = html; 424 + 425 + // Highlight code blocks 426 + body.querySelectorAll("pre code").forEach(block => { 427 + hljs.highlightElement(block); 428 + }); 429 + 430 + panel.classList.add("open"); 431 + } 432 + 433 + function deselectAll() { 434 + if (selectedEl) { 435 + selectedEl.classList.remove("selected"); 436 + selectedEl = null; 437 + } 438 + document.getElementById("detail-panel").classList.remove("open"); 439 + } 440 + 441 + document.getElementById("close-btn").addEventListener("click", deselectAll); 442 + 443 + function escapeText(s) { 444 + const d = document.createElement("div"); 445 + d.textContent = s; 446 + return d.innerHTML; 447 + } 448 + 449 + function escapeAttr(s) { 450 + return s.replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/'/g,"&#39;").replace(/</g,"&lt;").replace(/>/g,"&gt;"); 451 + } 452 + 453 + init(); 454 + </script> 455 + </body> 456 + </html>`; 457 + } 458 + 459 + function escapeHTML(str: string): string { 460 + return str 461 + .replace(/&/g, "&amp;") 462 + .replace(/</g, "&lt;") 463 + .replace(/>/g, "&gt;") 464 + .replace(/"/g, "&quot;"); 465 + }
+18
src/types.ts
··· 1 + export interface NodeLink { 2 + label: string; 3 + url: string; 4 + } 5 + 6 + export interface NodeMetadata { 7 + title: string; 8 + description: string; 9 + links?: NodeLink[]; 10 + codeSnippet?: string; 11 + threadID?: string; 12 + } 13 + 14 + export interface WalkthroughDiagram { 15 + code: string; 16 + summary: string; 17 + nodes: Record<string, NodeMetadata>; 18 + }
+29
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + // Environment setup & latest features 4 + "lib": ["ESNext"], 5 + "target": "ESNext", 6 + "module": "Preserve", 7 + "moduleDetection": "force", 8 + "jsx": "react-jsx", 9 + "allowJs": true, 10 + 11 + // Bundler mode 12 + "moduleResolution": "bundler", 13 + "allowImportingTsExtensions": true, 14 + "verbatimModuleSyntax": true, 15 + "noEmit": true, 16 + 17 + // Best practices 18 + "strict": true, 19 + "skipLibCheck": true, 20 + "noFallthroughCasesInSwitch": true, 21 + "noUncheckedIndexedAccess": true, 22 + "noImplicitOverride": true, 23 + 24 + // Some stricter flags (disabled by default) 25 + "noUnusedLocals": false, 26 + "noUnusedParameters": false, 27 + "noPropertyAccessFromIndexSignature": false 28 + } 29 + }