katman-mcp 0.1.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.
Files changed (97) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +160 -0
  3. package/dist/cli.js +71 -0
  4. package/dist/context.js +43 -0
  5. package/dist/crawlers.js +86 -0
  6. package/dist/index.js +25 -0
  7. package/dist/license.js +448 -0
  8. package/dist/meta.js +28 -0
  9. package/dist/net.js +128 -0
  10. package/dist/server.js +17 -0
  11. package/dist/state.js +81 -0
  12. package/dist/tools/account/account.js +64 -0
  13. package/dist/tools/account/cli.js +21 -0
  14. package/dist/tools/account/index.js +48 -0
  15. package/dist/tools/account/sync.js +142 -0
  16. package/dist/tools/account/upsell.js +290 -0
  17. package/dist/tools/crawl/audit/analyze.js +719 -0
  18. package/dist/tools/crawl/audit/bots.js +114 -0
  19. package/dist/tools/crawl/audit/checks.js +126 -0
  20. package/dist/tools/crawl/audit/html.js +382 -0
  21. package/dist/tools/crawl/audit/index.js +364 -0
  22. package/dist/tools/crawl/audit/model.js +1 -0
  23. package/dist/tools/crawl/audit/report.js +199 -0
  24. package/dist/tools/crawl/audit/robots.js +199 -0
  25. package/dist/tools/crawl/audit/sitemap.js +147 -0
  26. package/dist/tools/crawl/cli.js +106 -0
  27. package/dist/tools/crawl/common/advice.js +61 -0
  28. package/dist/tools/crawl/common/findings.js +57 -0
  29. package/dist/tools/crawl/common/routes.js +80 -0
  30. package/dist/tools/crawl/common/text.js +89 -0
  31. package/dist/tools/crawl/common/urls.js +64 -0
  32. package/dist/tools/crawl/index.js +226 -0
  33. package/dist/tools/crawl/indexnow.js +271 -0
  34. package/dist/tools/crawl/keywords.js +178 -0
  35. package/dist/tools/crawl/scan/framework.js +645 -0
  36. package/dist/tools/crawl/scan/index.js +175 -0
  37. package/dist/tools/crawl/scan/project.js +180 -0
  38. package/dist/tools/crawl/scan/routes.js +354 -0
  39. package/dist/tools/crawl/scan/signals.js +414 -0
  40. package/dist/tools/method/cli.js +42 -0
  41. package/dist/tools/method/core/brief.js +302 -0
  42. package/dist/tools/method/core/extract.js +435 -0
  43. package/dist/tools/method/core/generate.js +504 -0
  44. package/dist/tools/method/core/lint.js +556 -0
  45. package/dist/tools/method/core/plan.js +405 -0
  46. package/dist/tools/method/core/prompt.js +150 -0
  47. package/dist/tools/method/core/questions.js +196 -0
  48. package/dist/tools/method/core/report.js +182 -0
  49. package/dist/tools/method/core/research-run.js +148 -0
  50. package/dist/tools/method/core/research.js +448 -0
  51. package/dist/tools/method/core/snippets.js +292 -0
  52. package/dist/tools/method/core/start.js +277 -0
  53. package/dist/tools/method/data/anatomy.js +141 -0
  54. package/dist/tools/method/data/appendices.js +202 -0
  55. package/dist/tools/method/data/cloudflare.js +192 -0
  56. package/dist/tools/method/data/evidence.js +137 -0
  57. package/dist/tools/method/data/geo-jobs.js +128 -0
  58. package/dist/tools/method/data/index.js +19 -0
  59. package/dist/tools/method/data/landing.js +67 -0
  60. package/dist/tools/method/data/lessons.js +127 -0
  61. package/dist/tools/method/data/measurement.js +142 -0
  62. package/dist/tools/method/data/method.js +141 -0
  63. package/dist/tools/method/data/offsite.js +148 -0
  64. package/dist/tools/method/data/pages.js +167 -0
  65. package/dist/tools/method/data/prompts.js +633 -0
  66. package/dist/tools/method/data/publish.js +113 -0
  67. package/dist/tools/method/data/render.js +117 -0
  68. package/dist/tools/method/data/research.js +131 -0
  69. package/dist/tools/method/data/roadmap.js +105 -0
  70. package/dist/tools/method/data/traps.js +74 -0
  71. package/dist/tools/method/data/verify.js +88 -0
  72. package/dist/tools/method/data/viral-list.js +39 -0
  73. package/dist/tools/method/index.js +209 -0
  74. package/dist/tools/method/lib/calls.js +53 -0
  75. package/dist/tools/method/lib/framework.js +525 -0
  76. package/dist/tools/method/lib/l10n.js +17 -0
  77. package/dist/tools/method/lib/routes.js +118 -0
  78. package/dist/tools/method/lib/state.js +454 -0
  79. package/dist/tools/method/lib/text.js +165 -0
  80. package/dist/tools/method/mcp/prompts.js +79 -0
  81. package/dist/tools/method/mcp/resources.js +50 -0
  82. package/dist/tools/visibility/analyze.js +463 -0
  83. package/dist/tools/visibility/cli.js +86 -0
  84. package/dist/tools/visibility/estimate.js +105 -0
  85. package/dist/tools/visibility/index.js +108 -0
  86. package/dist/tools/visibility/judge.js +156 -0
  87. package/dist/tools/visibility/manual.js +275 -0
  88. package/dist/tools/visibility/models.js +277 -0
  89. package/dist/tools/visibility/monitoring.js +165 -0
  90. package/dist/tools/visibility/openrouter.js +449 -0
  91. package/dist/tools/visibility/render.js +295 -0
  92. package/dist/tools/visibility/run.js +684 -0
  93. package/dist/tools/visibility/summary.js +164 -0
  94. package/dist/types.js +3 -0
  95. package/dist/util.js +92 -0
  96. package/package.json +54 -0
  97. package/server.json +34 -0
@@ -0,0 +1,354 @@
1
+ import { classifyPath } from "../common/routes.js";
2
+ import { escapeRe } from "../common/text.js";
3
+ import { codeFiles, lineOf, list, text } from "./project.js";
4
+ const FILE_LIKE = /\.(txt|xml|json|webmanifest|ico|png|svg|jpg|jpeg|webp|rss|atom|js|css|map|pdf)$/i;
5
+ function add(out, path, file, fileRoutes, line = 1) {
6
+ let p = path.trim();
7
+ if (!p)
8
+ p = "/";
9
+ if (!p.startsWith("/"))
10
+ p = "/" + p;
11
+ p = p.replace(/\/{2,}/g, "/");
12
+ if (p.length > 1)
13
+ p = p.replace(/\/$/, "");
14
+ if (/[`${}]/.test(p) && /\$\{/.test(p))
15
+ return; // template literal
16
+ const last = p.split("/").pop() ?? "";
17
+ if (FILE_LIKE.test(last) && !/[[:$*]/.test(last)) {
18
+ if (!fileRoutes.has(last.toLowerCase()))
19
+ fileRoutes.set(last.toLowerCase(), { file: file ?? "", line, match: p });
20
+ return;
21
+ }
22
+ if (out.has(p))
23
+ return;
24
+ out.set(p, { path: p, file, kind: classifyPath(p), dynamic: /[[:$*]|\(\.\.\.|\?$/.test(p) });
25
+ }
26
+ function segsToPath(segs) {
27
+ return "/" + segs.filter(Boolean).join("/");
28
+ }
29
+ function nextApp(rel) {
30
+ const m = rel.match(/(?:^|\/)app\/(.*?)\/?(?:page)\.(?:tsx|ts|jsx|js|mdx|md)$/);
31
+ if (!m)
32
+ return null;
33
+ const segs = m[1].split("/").filter(Boolean);
34
+ if (segs.some((s) => s.startsWith("_") || /^\(\.{1,3}\)/.test(s)))
35
+ return null;
36
+ return segsToPath(segs.filter((s) => !/^\(.*\)$/.test(s) && !s.startsWith("@")));
37
+ }
38
+ function stripIndex(segs) {
39
+ const out = [...segs];
40
+ if (out.length && /^index$/i.test(out[out.length - 1]))
41
+ out.pop();
42
+ return out;
43
+ }
44
+ function fileSegs(rel, base) {
45
+ const m = rel.match(base);
46
+ if (!m)
47
+ return null;
48
+ return m[1].replace(/\.[a-z0-9]+$/i, "").split("/").filter(Boolean);
49
+ }
50
+ export function remixFlat(name) {
51
+ const parts = [];
52
+ let cur = "";
53
+ let inBr = false;
54
+ for (const ch of name) {
55
+ if (ch === "[") {
56
+ inBr = true;
57
+ continue;
58
+ }
59
+ if (ch === "]") {
60
+ inBr = false;
61
+ continue;
62
+ }
63
+ if (ch === "." && !inBr) {
64
+ parts.push(cur);
65
+ cur = "";
66
+ continue;
67
+ }
68
+ cur += ch;
69
+ }
70
+ parts.push(cur);
71
+ const out = [];
72
+ let dynamic = false;
73
+ for (let s of parts) {
74
+ if (!s || s === "_index" || s === "route")
75
+ continue;
76
+ if (s.startsWith("_"))
77
+ continue;
78
+ if (s.endsWith("_"))
79
+ s = s.slice(0, -1);
80
+ if (s === "$") {
81
+ out.push("*");
82
+ dynamic = true;
83
+ }
84
+ else if (/^\(\$.+\)$/.test(s)) {
85
+ out.push(`:${s.slice(2, -1)}?`);
86
+ dynamic = true;
87
+ }
88
+ else if (/^\(.+\)$/.test(s))
89
+ out.push(`${s.slice(1, -1)}?`);
90
+ else if (s.startsWith("$")) {
91
+ out.push(`:${s.slice(1)}`);
92
+ dynamic = true;
93
+ }
94
+ else
95
+ out.push(s);
96
+ }
97
+ return { path: segsToPath(out), dynamic };
98
+ }
99
+ const ROUTER_LIB = /react-router|@tanstack\/react-router|@tanstack\/solid-router|vue-router|@angular\/router|@solidjs\/router|wouter|preact-router|svelte-spa-router|createBrowserRouter|RouterModule|provideRouter|<Routes\b/;
100
+ export function scanRoutes(p, det) {
101
+ const out = new Map();
102
+ const fileRoutes = new Map();
103
+ const hashRouting = [];
104
+ const catchAll = [];
105
+ const pre = det.appDir ? `${det.appDir}/` : "";
106
+ const at = (s) => new RegExp(`^${escapeRe(pre)}${s}`);
107
+ const fw = det.framework.name;
108
+ const code = codeFiles(p);
109
+ if (fw === "next") {
110
+ for (const f of list(p, at(String.raw `(src/)?app/(.+/)?page\.(tsx|ts|jsx|js|mdx|md)$`))) {
111
+ const r = nextApp(f);
112
+ if (r !== null)
113
+ add(out, r, f, fileRoutes);
114
+ }
115
+ for (const f of list(p, at(String.raw `(src/)?app/(.+/)?route\.(ts|js)$`))) {
116
+ const m = f.match(/(?:^|\/)app\/(.*?)\/route\.(ts|js)$/);
117
+ if (m)
118
+ add(out, segsToPath(m[1].split("/").filter((s) => !/^\(.*\)$/.test(s))), f, fileRoutes);
119
+ }
120
+ for (const f of list(p, at(String.raw `(src/)?pages/.+\.(tsx|ts|jsx|js|mdx|md)$`))) {
121
+ const segs = fileSegs(f, /(?:^|\/)pages\/(.+)$/);
122
+ if (!segs || /^_(app|document|error)$/.test(segs[segs.length - 1]) || /^(404|500)$/.test(segs.join("/")))
123
+ continue;
124
+ add(out, segsToPath(stripIndex(segs)), f, fileRoutes);
125
+ }
126
+ }
127
+ else if (fw === "astro") {
128
+ for (const f of list(p, at(String.raw `src/pages/.+\.(astro|md|mdx|html|markdown|ts|js)$`))) {
129
+ const segs = fileSegs(f, /src\/pages\/(.+)$/);
130
+ if (!segs || segs.some((s) => s.startsWith("_")))
131
+ continue;
132
+ const endpoint = /\.(ts|js)$/.test(f);
133
+ const last = f.split("/").pop().replace(/\.(ts|js)$/, "");
134
+ if (endpoint && FILE_LIKE.test(last)) {
135
+ fileRoutes.set(last.toLowerCase(), { file: f, line: 1, match: last });
136
+ continue;
137
+ }
138
+ if (/^404$/.test(segs.join("/")))
139
+ continue;
140
+ add(out, segsToPath(stripIndex(segs)), f, fileRoutes);
141
+ }
142
+ }
143
+ else if (fw === "nuxt") {
144
+ for (const f of list(p, at(String.raw `(app/)?pages/.+\.vue$`))) {
145
+ const segs = fileSegs(f, /(?:^|\/)pages\/(.+)$/);
146
+ if (segs)
147
+ add(out, segsToPath(stripIndex(segs)), f, fileRoutes);
148
+ }
149
+ for (const f of list(p, at(String.raw `server/(api|routes)/.+\.(ts|js)$`))) {
150
+ const m = f.match(/server\/(api|routes)\/(.+)\.(ts|js)$/);
151
+ if (m)
152
+ add(out, (m[1] === "api" ? "/api/" : "/") + m[2].replace(/(^|\/)index$/, "").replace(/\.(get|post|put|delete)$/, ""), f, fileRoutes);
153
+ }
154
+ }
155
+ else if (fw === "sveltekit") {
156
+ for (const f of list(p, at(String.raw `src/routes/(.*/)?\+(page|server)\.(svelte|md|svx|ts|js)$`))) {
157
+ const m = f.match(/src\/routes\/(.*?)\/?\+(page|server)\./);
158
+ if (!m)
159
+ continue;
160
+ const segs = m[1].split("/").filter((s) => s && !/^\(.*\)$/.test(s));
161
+ add(out, segsToPath(segs), f, fileRoutes);
162
+ }
163
+ }
164
+ else if (fw === "remix" || fw === "react-router") {
165
+ const cfgRel = list(p, at(String.raw `app/routes\.(ts|js|mts)$`))[0];
166
+ const cfg = cfgRel ? text(p, cfgRel) : "";
167
+ if (cfg && !/flatRoutes\s*\(/.test(cfg)) {
168
+ for (const m of cfg.matchAll(/\broute\(\s*["'`]([^"'`]*)["'`]\s*,\s*["'`]([^"'`]+)["'`]/g))
169
+ add(out, m[1], `${pre}app/${m[2]}`, fileRoutes);
170
+ for (const m of cfg.matchAll(/\bindex\(\s*["'`]([^"'`]+)["'`]/g))
171
+ add(out, "/", `${pre}app/${m[1]}`, fileRoutes);
172
+ }
173
+ for (const f of list(p, at(String.raw `app/routes/[^/]+\.(tsx|ts|jsx|js|mdx|md)$`))) {
174
+ const name = f.split("/").pop().replace(/\.(tsx|ts|jsx|js|mdx|md)$/, "");
175
+ const r = remixFlat(name);
176
+ if (!/export\s+default/.test(text(p, f)) && /\.(ts|js)$/.test(f)) {
177
+ const last = r.path.split("/").pop() ?? "";
178
+ if (FILE_LIKE.test(last))
179
+ fileRoutes.set(last.toLowerCase(), { file: f, line: 1, match: r.path });
180
+ else
181
+ add(out, r.path, f, fileRoutes);
182
+ continue;
183
+ }
184
+ add(out, r.path, f, fileRoutes);
185
+ }
186
+ for (const f of list(p, at(String.raw `app/routes/[^/]+/route\.(tsx|ts|jsx|js)$`))) {
187
+ const name = f.split("/").slice(-2, -1)[0];
188
+ add(out, remixFlat(name).path, f, fileRoutes);
189
+ }
190
+ }
191
+ else if (fw === "gatsby") {
192
+ for (const f of list(p, at(String.raw `src/pages/.+\.(tsx|ts|jsx|js)$`))) {
193
+ const segs = fileSegs(f, /src\/pages\/(.+)$/);
194
+ if (!segs || /^404$/.test(segs.join("/")))
195
+ continue;
196
+ add(out, segsToPath(stripIndex(segs)), f, fileRoutes);
197
+ }
198
+ }
199
+ else if (fw === "hugo") {
200
+ for (const f of list(p, /^content\/.+\.(md|markdown|html)$/)) {
201
+ const segs = fileSegs(f, /^content\/(.+)$/);
202
+ const last = segs[segs.length - 1];
203
+ if (last === "_index" || last === "index")
204
+ segs.pop();
205
+ add(out, segsToPath(segs), f, fileRoutes);
206
+ }
207
+ }
208
+ else if (fw === "jekyll") {
209
+ for (const f of list(p, /^[^_/][^/]*\.(md|markdown|html)$/)) {
210
+ if (/^(README|CHANGELOG|LICENSE|404)\./i.test(f))
211
+ continue;
212
+ add(out, segsToPath(stripIndex([f.replace(/\.(md|markdown|html)$/, "")])), f, fileRoutes);
213
+ }
214
+ for (const f of list(p, /^_posts\/.+\.(md|markdown|html)$/)) {
215
+ const slug = f.split("/").pop().replace(/^\d{4}-\d{2}-\d{2}-/, "").replace(/\.(md|markdown|html)$/, "");
216
+ add(out, `/${slug}`, f, fileRoutes);
217
+ }
218
+ }
219
+ else if (fw === "eleventy" || fw === "static-html" || fw === "vitepress" || fw === "docusaurus") {
220
+ const input = fw === "eleventy" && p.dirs.has("src") ? "src/" : fw === "vitepress" ? "docs/" : fw === "docusaurus" ? "" : pre;
221
+ const re = fw === "static-html" ? /\.html?$/ : /\.(md|njk|liquid|html|11ty\.js|mdx)$/;
222
+ for (const f of p.files.keys()) {
223
+ if (!f.startsWith(input) || !re.test(f))
224
+ continue;
225
+ if (/(^|\/)(_includes|_data|_layouts|node_modules|\.vitepress)\//.test(f) || /(^|\/)(README|CHANGELOG|LICENSE)\.md$/i.test(f))
226
+ continue;
227
+ if (fw === "docusaurus" && !/^(docs|blog|src\/pages)\//.test(f))
228
+ continue;
229
+ const segs = f.slice(input.length).replace(/\.(html?|md|njk|liquid|11ty\.js|mdx)$/, "").split("/");
230
+ if (/^404$/.test(segs.join("/")))
231
+ continue;
232
+ add(out, segsToPath(stripIndex(segs)), f, fileRoutes);
233
+ }
234
+ }
235
+ else if (fw === "tanstack-start") {
236
+ for (const f of code) {
237
+ for (const m of f.text.matchAll(/createFileRoute\(\s*["'`]([^"'`]+)["'`]\s*\)/g))
238
+ add(out, tanstackPath(m[1]), f.rel, fileRoutes, lineOf(f.text, m.index ?? 0));
239
+ }
240
+ }
241
+ // SPA routers (also used inside other frameworks' client code)
242
+ if (!out.size || ["vite-react", "vite-vue", "vite-svelte", "vite-solid", "vite", "cra", "angular", "static-html", "unknown", "tanstack-start"].includes(fw)) {
243
+ spaRoutes(code, out, fileRoutes, catchAll, fw);
244
+ }
245
+ // Hand-written servers (Workers / Express / Hono) and path tables
246
+ if (fw === "custom-server" || fw === "unknown" || !out.size)
247
+ serverRoutes(code, out, fileRoutes);
248
+ // Prebuilt HTML served from the static folder by a custom server (e.g. public/pricing.html → /pricing)
249
+ if (fw === "custom-server" && det.staticDir) {
250
+ const pretty = det.hosting.provider === "cloudflare" || det.hosting.provider === "vercel" || det.hosting.provider === "netlify";
251
+ for (const f of p.files.keys()) {
252
+ if (!f.startsWith(det.staticDir + "/") || !/\.html?$/.test(f))
253
+ continue;
254
+ const rel = f.slice(det.staticDir.length + 1);
255
+ if (/(^|\/)(404|500)\.html?$/.test(rel))
256
+ continue;
257
+ const segs = rel.split("/");
258
+ const last = segs.pop();
259
+ if (!/^index\.html?$/.test(last))
260
+ segs.push(pretty ? last.replace(/\.html?$/, "") : last);
261
+ add(out, segsToPath(segs), f, fileRoutes);
262
+ }
263
+ }
264
+ // Machine files served from code in any framework (robots.txt / sitemap.xml / llms.txt routes)
265
+ collectFileRoutes(code, fileRoutes);
266
+ // Hash routing and catch-all routes
267
+ for (const f of code) {
268
+ const t = f.text;
269
+ for (const m of t.matchAll(/\b(HashRouter|createHashRouter|createWebHashHistory|useHash\s*:\s*true|withHashLocation|hashHistory|createHashHistory)\b/g)) {
270
+ hashRouting.push({ file: f.rel, line: lineOf(t, m.index ?? 0), match: m[1] });
271
+ }
272
+ if (/from\s+["']svelte-spa-router["']/.test(t))
273
+ hashRouting.push({ file: f.rel, line: 1, match: "svelte-spa-router (hash-based)" });
274
+ }
275
+ return { routes: [...out.values()], hashRouting, catchAll, fileRoutes };
276
+ }
277
+ function tanstackPath(p) {
278
+ const segs = p.split("/").filter((s) => s && !s.startsWith("_") && !/^\(.*\)$/.test(s));
279
+ return segsToPath(segs);
280
+ }
281
+ function spaRoutes(code, out, fileRoutes, catchAll, fw) {
282
+ for (const f of code) {
283
+ const t = f.text;
284
+ const isRouterFile = ROUTER_LIB.test(t);
285
+ for (const m of t.matchAll(/<Route\b[^>]*?\bpath\s*=\s*(?:\{\s*)?["'`]([^"'`]*)["'`]/g)) {
286
+ const line = lineOf(t, m.index ?? 0);
287
+ if (m[1] === "*" || m[1] === "/*")
288
+ catchAll.push({ file: f.rel, line, match: `path="${m[1]}"` });
289
+ else
290
+ add(out, m[1], f.rel, fileRoutes, line);
291
+ }
292
+ if (isRouterFile) {
293
+ for (const m of t.matchAll(/\bpath\s*:\s*["'`]([^"'`]*)["'`]/g)) {
294
+ const line = lineOf(t, m.index ?? 0);
295
+ const v = m[1];
296
+ if (v === "*" || v === "**" || v === "/*" || /pathMatch\(\.\*\)/.test(v) || v === "/:catchAll(.*)")
297
+ catchAll.push({ file: f.rel, line, match: `path: "${v}"` });
298
+ else if (fw === "angular" || !v.startsWith("/"))
299
+ add(out, v, f.rel, fileRoutes, line);
300
+ else
301
+ add(out, v, f.rel, fileRoutes, line);
302
+ }
303
+ for (const m of t.matchAll(/createFileRoute\(\s*["'`]([^"'`]+)["'`]\s*\)/g))
304
+ add(out, tanstackPath(m[1]), f.rel, fileRoutes, lineOf(t, m.index ?? 0));
305
+ }
306
+ if (/from\s+["']svelte-spa-router["']/.test(t)) {
307
+ for (const m of t.matchAll(/["'`](\/[^"'`]*)["'`]\s*:\s*[A-Za-z_]\w*/g)) {
308
+ if (m[1] === "*")
309
+ catchAll.push({ file: f.rel, line: lineOf(t, m.index ?? 0), match: "*" });
310
+ else
311
+ add(out, m[1], f.rel, fileRoutes, lineOf(t, m.index ?? 0));
312
+ }
313
+ }
314
+ }
315
+ }
316
+ const SERVER_CALL = /\b(?:app|router|r|route|routes|server|api|hono|fastify|site|web)\.(?:get|all|on|route)\(\s*(?:["'`](?:GET|HEAD)["'`]\s*,\s*)?["'`](\/[^"'`\s]*)["'`]/g;
317
+ const PATHNAME_EQ = /\b(?:pathname|url\.pathname)\s*===?\s*["'`](\/[^"'`\s]*)["'`]/g;
318
+ const CASE_PATH = /case\s+["'`](\/[^"'`\s]*)["'`]\s*:/g;
319
+ const PATH_TABLE_FILE = /(^|\/)(paths?|routes?|urls?|pages|site-?map|nav|navigation|links|menu)\.(m?[jt]sx?|cjs)$/i;
320
+ const NOT_ROUTES_FILE = /(^|\/)(__mocks__|mocks?|fakes?|stubs?|fixtures?|seeds?|samples?)(\/|[._-])|(^|\/)[^/]*(mock|fake|stub|dev[-_.]?server)[^/]*\.[a-z]+$/i;
321
+ function serverRoutes(code, out, fileRoutes) {
322
+ for (const f of code) {
323
+ if (!/\.(m?[jt]sx?|cjs)$/.test(f.rel) || NOT_ROUTES_FILE.test(f.rel))
324
+ continue;
325
+ const t = f.text;
326
+ for (const re of [SERVER_CALL, PATHNAME_EQ, CASE_PATH]) {
327
+ for (const m of t.matchAll(re))
328
+ add(out, m[1], f.rel, fileRoutes, lineOf(t, m.index ?? 0));
329
+ }
330
+ if (PATH_TABLE_FILE.test(f.rel)) {
331
+ for (const m of t.matchAll(/["'`](\/|\/[a-z0-9][a-z0-9\-/]*)["'`]/g)) {
332
+ if (/\/(static|assets|img|images|fonts|css|js)\//.test(m[1]))
333
+ continue;
334
+ add(out, m[1], f.rel, fileRoutes, lineOf(t, m.index ?? 0));
335
+ }
336
+ }
337
+ }
338
+ }
339
+ function collectFileRoutes(code, fileRoutes) {
340
+ const names = ["robots.txt", "sitemap.xml", "sitemap-index.xml", "sitemap_index.xml", "llms.txt", "llms-full.txt"];
341
+ for (const f of code) {
342
+ if (!/\.(m?[jt]sx?|cjs)$/.test(f.rel))
343
+ continue;
344
+ const t = f.text;
345
+ for (const n of names) {
346
+ if (fileRoutes.has(n))
347
+ continue;
348
+ const re = new RegExp(`(?:\\.(?:get|all)\\(|pathname\\s*===?\\s*|case\\s+|path\\s*:\\s*)["'\`]/${escapeRe(n)}["'\`]`);
349
+ const m = t.match(re);
350
+ if (m)
351
+ fileRoutes.set(n, { file: f.rel, line: lineOf(t, m.index ?? 0), match: `/${n}` });
352
+ }
353
+ }
354
+ }