tangled
alpha
login
or
join now
dunkirk.sh
/
traverse
1
fork
atom
snatching amp's walkthrough for my own purposes mwhahaha
traverse.dunkirk.sh/diagram/6121f05c-a5ef-4ecf-8ffc-02534c5e767c
1
fork
atom
overview
issues
pulls
pipelines
feat: allow multiple clients
dunkirk.sh
1 month ago
2f238620
c4e2a084
verified
This commit was signed with the committer's
known signature
.
dunkirk.sh
SSH Key Fingerprint:
SHA256:DqcG0RXYExE26KiWo3VxJnsxswN1QNfTBvB+bdSpk80=
+187
-94
3 changed files
expand all
collapse all
unified
split
src
index.ts
storage.ts
template.ts
+142
-93
src/index.ts
···
3
import { z } from "zod/v4";
4
import { generateViewerHTML } from "./template.ts";
5
import type { WalkthroughDiagram } from "./types.ts";
6
-
import { initDb, loadAllDiagrams, saveDiagram, deleteDiagramFromDb, generateId } from "./storage.ts";
7
import { loadConfig } from "./config.ts";
8
9
const PORT = parseInt(process.env.TRAVERSE_PORT || "4173", 10);
···
18
const diagrams = loadAllDiagrams();
19
20
// --- Web server for serving interactive diagrams ---
21
-
Bun.serve({
22
-
port: PORT,
23
-
async fetch(req) {
24
-
const url = new URL(req.url);
25
-
const diagramMatch = url.pathname.match(/^\/diagram\/([\w-]+)$/);
0
0
0
26
27
-
if (diagramMatch) {
28
-
const id = diagramMatch[1]!;
29
-
const diagram = diagrams.get(id);
30
-
if (!diagram) {
31
-
return new Response(generate404HTML("Diagram not found", "This diagram doesn't exist or may have expired."), {
32
-
status: 404,
0
0
0
0
0
0
0
0
0
0
33
headers: { "Content-Type": "text/html; charset=utf-8" },
34
});
35
}
36
-
return new Response(generateViewerHTML(diagram, GIT_HASH, process.cwd(), {
37
-
mode: MODE,
38
-
shareServerUrl: config.shareServerUrl,
39
-
diagramId: id,
40
-
}), {
41
-
headers: { "Content-Type": "text/html; charset=utf-8" },
42
-
});
43
-
}
44
45
-
// DELETE /api/diagrams/:id
46
-
const apiMatch = url.pathname.match(/^\/api\/diagrams\/([\w-]+)$/);
47
-
if (apiMatch && req.method === "DELETE") {
48
-
const id = apiMatch[1]!;
49
-
if (!diagrams.has(id)) {
50
-
return Response.json({ error: "not found" }, { status: 404 });
0
0
0
0
51
}
52
-
diagrams.delete(id);
53
-
deleteDiagramFromDb(id);
54
-
return Response.json({ ok: true, id });
55
-
}
56
57
-
// POST /api/diagrams (server mode: accept diagrams from remote)
58
-
if (url.pathname === "/api/diagrams" && req.method === "POST") {
59
-
if (MODE !== "server") {
60
-
return Response.json({ error: "POST only available in server mode" }, { status: 403 });
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
61
}
62
-
try {
63
-
const body = await req.json() as WalkthroughDiagram;
64
-
if (!body.code || !body.summary || !body.nodes) {
65
-
return Response.json({ error: "missing required fields: code, summary, nodes" }, { status: 400 });
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
66
}
67
-
const id = generateId();
68
-
const diagram: WalkthroughDiagram = {
69
-
code: body.code,
70
-
summary: body.summary,
71
-
nodes: body.nodes,
72
-
createdAt: new Date().toISOString(),
73
-
};
74
-
diagrams.set(id, diagram);
75
-
saveDiagram(id, diagram);
76
-
const diagramUrl = `${url.origin}/diagram/${id}`;
77
-
return Response.json({ id, url: diagramUrl }, {
78
-
status: 201,
79
headers: {
80
"Access-Control-Allow-Origin": "*",
0
0
81
},
82
});
83
-
} catch {
84
-
return Response.json({ error: "invalid JSON body" }, { status: 400 });
85
}
86
-
}
87
88
-
// OPTIONS /api/diagrams — CORS preflight
89
-
if (url.pathname === "/api/diagrams" && req.method === "OPTIONS") {
90
-
return new Response(null, {
91
-
status: 204,
92
-
headers: {
93
-
"Access-Control-Allow-Origin": "*",
94
-
"Access-Control-Allow-Methods": "POST, OPTIONS",
95
-
"Access-Control-Allow-Headers": "Content-Type",
96
-
},
97
-
});
98
-
}
99
100
-
if (url.pathname === "/icon.svg") {
101
-
return new Response(Bun.file(import.meta.dir + "/../icon.svg"), {
102
-
headers: { "Content-Type": "image/svg+xml" },
103
-
});
104
-
}
0
0
0
0
105
106
-
// List available diagrams
107
-
if (url.pathname === "/") {
108
-
const html = MODE === "server"
109
-
? generateServerIndexHTML(diagrams.size, GIT_HASH)
110
-
: generateLocalIndexHTML(diagrams, GIT_HASH);
111
-
return new Response(html, {
112
headers: { "Content-Type": "text/html; charset=utf-8" },
113
});
114
-
}
115
-
116
-
return new Response(generate404HTML("Page not found", "There's nothing at this URL."), {
117
-
status: 404,
118
-
headers: { "Content-Type": "text/html; charset=utf-8" },
119
-
});
120
-
},
121
-
});
122
123
// --- MCP Server (local mode only) ---
124
if (MODE === "local") {
···
158
nodes: z.record(z.string(), nodeMetadataSchema),
159
}),
160
}, async ({ code, summary, nodes }) => {
161
-
const id = generateId();
162
-
const diagram: WalkthroughDiagram = {
163
-
code,
164
-
summary,
165
-
nodes,
166
-
createdAt: new Date().toISOString(),
167
-
};
168
-
diagrams.set(id, diagram);
169
-
saveDiagram(id, diagram);
170
171
-
const diagramUrl = `http://localhost:${PORT}/diagram/${id}`;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
172
173
return {
174
content: [
···
3
import { z } from "zod/v4";
4
import { generateViewerHTML } from "./template.ts";
5
import type { WalkthroughDiagram } from "./types.ts";
6
+
import { initDb, loadAllDiagrams, saveDiagram, deleteDiagramFromDb, generateId, getSharedUrl, saveSharedUrl } from "./storage.ts";
7
import { loadConfig } from "./config.ts";
8
9
const PORT = parseInt(process.env.TRAVERSE_PORT || "4173", 10);
···
18
const diagrams = loadAllDiagrams();
19
20
// --- Web server for serving interactive diagrams ---
21
+
let isClient = false;
22
+
23
+
try {
24
+
Bun.serve({
25
+
port: PORT,
26
+
async fetch(req) {
27
+
const url = new URL(req.url);
28
+
const diagramMatch = url.pathname.match(/^\/diagram\/([\w-]+)$/);
29
30
+
if (diagramMatch) {
31
+
const id = diagramMatch[1]!;
32
+
const diagram = diagrams.get(id);
33
+
if (!diagram) {
34
+
return new Response(generate404HTML("Diagram not found", "This diagram doesn't exist or may have expired."), {
35
+
status: 404,
36
+
headers: { "Content-Type": "text/html; charset=utf-8" },
37
+
});
38
+
}
39
+
const existingShareUrl = getSharedUrl(id);
40
+
return new Response(generateViewerHTML(diagram, GIT_HASH, process.cwd(), {
41
+
mode: MODE,
42
+
shareServerUrl: config.shareServerUrl,
43
+
diagramId: id,
44
+
existingShareUrl: existingShareUrl ?? undefined,
45
+
}), {
46
headers: { "Content-Type": "text/html; charset=utf-8" },
47
});
48
}
0
0
0
0
0
0
0
0
49
50
+
// DELETE /api/diagrams/:id
51
+
const apiMatch = url.pathname.match(/^\/api\/diagrams\/([\w-]+)$/);
52
+
if (apiMatch && req.method === "DELETE") {
53
+
const id = apiMatch[1]!;
54
+
if (!diagrams.has(id)) {
55
+
return Response.json({ error: "not found" }, { status: 404 });
56
+
}
57
+
diagrams.delete(id);
58
+
deleteDiagramFromDb(id);
59
+
return Response.json({ ok: true, id });
60
}
0
0
0
0
61
62
+
// POST /api/diagrams/:id/shared-url — save a shared URL for a local diagram
63
+
const sharedUrlMatch = url.pathname.match(/^\/api\/diagrams\/([\w-]+)\/shared-url$/);
64
+
if (sharedUrlMatch && req.method === "POST") {
65
+
const id = sharedUrlMatch[1]!;
66
+
try {
67
+
const body = await req.json() as { url: string };
68
+
if (!body.url) {
69
+
return Response.json({ error: "missing required field: url" }, { status: 400 });
70
+
}
71
+
saveSharedUrl(id, body.url);
72
+
return Response.json({ ok: true, id, url: body.url });
73
+
} catch {
74
+
return Response.json({ error: "invalid JSON body" }, { status: 400 });
75
+
}
76
+
}
77
+
78
+
// GET /api/diagrams/:id/shared-url — retrieve a stored shared URL
79
+
if (sharedUrlMatch && req.method === "GET") {
80
+
const id = sharedUrlMatch[1]!;
81
+
const sharedUrl = getSharedUrl(id);
82
+
if (!sharedUrl) {
83
+
return Response.json({ url: null });
84
+
}
85
+
return Response.json({ url: sharedUrl });
86
}
87
+
88
+
// POST /api/diagrams — accept diagrams from remote or sibling instances
89
+
if (url.pathname === "/api/diagrams" && req.method === "POST") {
90
+
try {
91
+
const body = await req.json() as WalkthroughDiagram;
92
+
if (!body.code || !body.summary || !body.nodes) {
93
+
return Response.json({ error: "missing required fields: code, summary, nodes" }, { status: 400 });
94
+
}
95
+
const id = generateId();
96
+
const diagram: WalkthroughDiagram = {
97
+
code: body.code,
98
+
summary: body.summary,
99
+
nodes: body.nodes,
100
+
createdAt: new Date().toISOString(),
101
+
};
102
+
diagrams.set(id, diagram);
103
+
saveDiagram(id, diagram);
104
+
const diagramUrl = `${url.origin}/diagram/${id}`;
105
+
return Response.json({ id, url: diagramUrl }, {
106
+
status: 201,
107
+
headers: {
108
+
"Access-Control-Allow-Origin": "*",
109
+
},
110
+
});
111
+
} catch {
112
+
return Response.json({ error: "invalid JSON body" }, { status: 400 });
113
}
114
+
}
115
+
116
+
// OPTIONS /api/diagrams — CORS preflight
117
+
if (url.pathname === "/api/diagrams" && req.method === "OPTIONS") {
118
+
return new Response(null, {
119
+
status: 204,
0
0
0
0
0
0
120
headers: {
121
"Access-Control-Allow-Origin": "*",
122
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
123
+
"Access-Control-Allow-Headers": "Content-Type",
124
},
125
});
0
0
126
}
0
127
128
+
if (url.pathname === "/icon.svg") {
129
+
return new Response(Bun.file(import.meta.dir + "/../icon.svg"), {
130
+
headers: { "Content-Type": "image/svg+xml" },
131
+
});
132
+
}
0
0
0
0
0
0
133
134
+
// List available diagrams
135
+
if (url.pathname === "/") {
136
+
const html = MODE === "server"
137
+
? generateServerIndexHTML(diagrams.size, GIT_HASH)
138
+
: generateLocalIndexHTML(diagrams, GIT_HASH);
139
+
return new Response(html, {
140
+
headers: { "Content-Type": "text/html; charset=utf-8" },
141
+
});
142
+
}
143
144
+
return new Response(generate404HTML("Page not found", "There's nothing at this URL."), {
145
+
status: 404,
0
0
0
0
146
headers: { "Content-Type": "text/html; charset=utf-8" },
147
});
148
+
},
149
+
});
150
+
} catch {
151
+
isClient = true;
152
+
console.error(`Web server already running on port ${PORT}, running in client mode`);
153
+
}
0
0
154
155
// --- MCP Server (local mode only) ---
156
if (MODE === "local") {
···
190
nodes: z.record(z.string(), nodeMetadataSchema),
191
}),
192
}, async ({ code, summary, nodes }) => {
193
+
let diagramUrl: string;
0
0
0
0
0
0
0
0
194
195
+
if (isClient) {
196
+
// POST diagram to the existing web server instance
197
+
const res = await fetch(`http://localhost:${PORT}/api/diagrams`, {
198
+
method: "POST",
199
+
headers: { "Content-Type": "application/json" },
200
+
body: JSON.stringify({ code, summary, nodes }),
201
+
});
202
+
if (!res.ok) {
203
+
return {
204
+
content: [{ type: "text", text: `Failed to send diagram to server: ${res.statusText}` }],
205
+
};
206
+
}
207
+
const data = await res.json() as { id: string; url: string };
208
+
diagramUrl = data.url;
209
+
} else {
210
+
const id = generateId();
211
+
const diagram: WalkthroughDiagram = {
212
+
code,
213
+
summary,
214
+
nodes,
215
+
createdAt: new Date().toISOString(),
216
+
};
217
+
diagrams.set(id, diagram);
218
+
saveDiagram(id, diagram);
219
+
diagramUrl = `http://localhost:${PORT}/diagram/${id}`;
220
+
}
221
222
return {
223
content: [
+19
src/storage.ts
···
34
created_at TEXT
35
)
36
`);
0
0
0
0
0
0
0
37
return db;
38
}
39
···
55
56
export function deleteDiagramFromDb(id: string): void {
57
db.run("DELETE FROM diagrams WHERE id = ?", [id]);
0
0
0
0
0
0
0
0
0
0
0
0
58
}
59
60
export function generateId(): string {
···
34
created_at TEXT
35
)
36
`);
37
+
db.run(`
38
+
CREATE TABLE IF NOT EXISTS shared_urls (
39
+
local_id TEXT PRIMARY KEY,
40
+
remote_url TEXT,
41
+
shared_at TEXT
42
+
)
43
+
`);
44
return db;
45
}
46
···
62
63
export function deleteDiagramFromDb(id: string): void {
64
db.run("DELETE FROM diagrams WHERE id = ?", [id]);
65
+
}
66
+
67
+
export function getSharedUrl(localId: string): string | null {
68
+
const row = db.query("SELECT remote_url FROM shared_urls WHERE local_id = ?").get(localId) as { remote_url: string } | null;
69
+
return row?.remote_url ?? null;
70
+
}
71
+
72
+
export function saveSharedUrl(localId: string, remoteUrl: string): void {
73
+
db.run(
74
+
"INSERT OR REPLACE INTO shared_urls (local_id, remote_url, shared_at) VALUES (?, ?, ?)",
75
+
[localId, remoteUrl, new Date().toISOString()]
76
+
);
77
}
78
79
export function generateId(): string {
+26
-1
src/template.ts
···
4
mode?: "local" | "server";
5
shareServerUrl?: string;
6
diagramId?: string;
0
7
}
8
9
export function generateViewerHTML(diagram: WalkthroughDiagram, gitHash: string = "dev", projectRoot: string = "", options: ViewerOptions = {}): string {
10
const diagramJSON = JSON.stringify(diagram).replace(/<\//g, "<\\/");
11
-
const { mode = "local", shareServerUrl = "", diagramId = "" } = options;
12
13
return `<!DOCTYPE html>
14
<html lang="en">
···
648
const SHARE_SERVER_URL = ${JSON.stringify(shareServerUrl)};
649
const DIAGRAM_ID = ${JSON.stringify(diagramId)};
650
const VIEWER_MODE = ${JSON.stringify(mode)};
0
651
652
const COPY_ICON = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="5" width="8" height="8" rx="1.5"/><path d="M3 11V3a1.5 1.5 0 011.5-1.5H11"/></svg>';
653
const CHECK_ICON = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8.5l3.5 3.5 6.5-7"/></svg>';
···
935
shareBtn.addEventListener("click", async () => {
936
shareBtn.disabled = true;
937
try {
0
0
0
0
0
0
0
0
0
0
0
0
938
const res = await fetch(SHARE_SERVER_URL + "/api/diagrams", {
939
method: "POST",
940
headers: { "Content-Type": "application/json" },
···
943
if (!res.ok) throw new Error("Share failed");
944
const data = await res.json();
945
await navigator.clipboard.writeText(data.url);
0
0
0
0
0
0
0
0
0
0
0
946
shareBtn.querySelector("span").textContent = "Copied link!";
947
shareBtn.classList.add("shared");
948
setTimeout(() => {
···
4
mode?: "local" | "server";
5
shareServerUrl?: string;
6
diagramId?: string;
7
+
existingShareUrl?: string;
8
}
9
10
export function generateViewerHTML(diagram: WalkthroughDiagram, gitHash: string = "dev", projectRoot: string = "", options: ViewerOptions = {}): string {
11
const diagramJSON = JSON.stringify(diagram).replace(/<\//g, "<\\/");
12
+
const { mode = "local", shareServerUrl = "", diagramId = "", existingShareUrl = "" } = options;
13
14
return `<!DOCTYPE html>
15
<html lang="en">
···
649
const SHARE_SERVER_URL = ${JSON.stringify(shareServerUrl)};
650
const DIAGRAM_ID = ${JSON.stringify(diagramId)};
651
const VIEWER_MODE = ${JSON.stringify(mode)};
652
+
let EXISTING_SHARE_URL = ${JSON.stringify(existingShareUrl)};
653
654
const COPY_ICON = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="5" width="8" height="8" rx="1.5"/><path d="M3 11V3a1.5 1.5 0 011.5-1.5H11"/></svg>';
655
const CHECK_ICON = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8.5l3.5 3.5 6.5-7"/></svg>';
···
937
shareBtn.addEventListener("click", async () => {
938
shareBtn.disabled = true;
939
try {
940
+
// If we already have a shared URL, just copy it
941
+
if (EXISTING_SHARE_URL) {
942
+
await navigator.clipboard.writeText(EXISTING_SHARE_URL);
943
+
shareBtn.querySelector("span").textContent = "Copied link!";
944
+
shareBtn.classList.add("shared");
945
+
setTimeout(() => {
946
+
shareBtn.querySelector("span").textContent = "Share";
947
+
shareBtn.classList.remove("shared");
948
+
}, 2000);
949
+
return;
950
+
}
951
+
952
const res = await fetch(SHARE_SERVER_URL + "/api/diagrams", {
953
method: "POST",
954
headers: { "Content-Type": "application/json" },
···
957
if (!res.ok) throw new Error("Share failed");
958
const data = await res.json();
959
await navigator.clipboard.writeText(data.url);
960
+
961
+
// Save the shared URL locally so we don't re-upload next time
962
+
if (DIAGRAM_ID) {
963
+
fetch("/api/diagrams/" + DIAGRAM_ID + "/shared-url", {
964
+
method: "POST",
965
+
headers: { "Content-Type": "application/json" },
966
+
body: JSON.stringify({ url: data.url }),
967
+
}).catch(() => {}); // best-effort
968
+
EXISTING_SHARE_URL = data.url;
969
+
}
970
+
971
shareBtn.querySelector("span").textContent = "Copied link!";
972
shareBtn.classList.add("shared");
973
setTimeout(() => {