radiant-docs 0.1.66 → 0.1.68

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.
@@ -65,7 +65,7 @@ import { Icon } from "astro-icon/components";
65
65
  x-on:pointerdown="onOverlayPointerDown($event)"
66
66
  x-on:pointerup="onOverlayPointerUp($event)"
67
67
  x-on:pointercancel="backdropInteractionStarted = false"
68
- class="relative z-50 flex min-h-screen items-center justify-center p-2 xs:p-4 md:p-8"
68
+ class="relative z-50 flex h-dvh items-center justify-center p-2 xs:p-4 md:p-8"
69
69
  >
70
70
  <div
71
71
  x-on:click.stop
@@ -1062,6 +1062,14 @@ const headerValuePrefixes = Object.fromEntries(
1062
1062
  </div>
1063
1063
  </div>
1064
1064
 
1065
+ <style is:global>
1066
+ @media (max-width: 639px) {
1067
+ [data-playground-form-root] :is(input, select, textarea) {
1068
+ font-size: 16px;
1069
+ }
1070
+ }
1071
+ </style>
1072
+
1065
1073
  <script>
1066
1074
  import {
1067
1075
  highlightAssistantCodeToHtml,
@@ -75,6 +75,8 @@ const navigationTabs = config.navigation.tabs as
75
75
  const hasNavigationTabs =
76
76
  Array.isArray(navigationTabs?.items) && navigationTabs.items.length > 0;
77
77
  const tabsPresentation = navigationTabs?.presentation ?? "topbar";
78
+ const hasTopbarNavigationTabs =
79
+ hasNavigationTabs && tabsPresentation === "topbar";
78
80
  ---
79
81
 
80
82
  <!doctype html>
@@ -356,7 +358,10 @@ const tabsPresentation = navigationTabs?.presentation ?? "topbar";
356
358
 
357
359
  <!-- Main Content -->
358
360
  <div
359
- class="mx-1 mt-1 px-4 sm:px-6 lg:pl-[calc(288px+32px)] pt-16 lg:pr-8 bg-background"
361
+ class:list={[
362
+ "mx-1 mt-1 px-4 sm:px-6 lg:pl-[calc(288px+32px)] pt-16 lg:pr-8 bg-background",
363
+ hasTopbarNavigationTabs && "lg:pt-[108px]",
364
+ ]}
360
365
  >
361
366
  <main class="mx-auto pt-16 pb-20 min-h-[calc(100vh-64px)]">
362
367
  <slot />
@@ -9,6 +9,7 @@ import {
9
9
  type MdxRoute,
10
10
  type OpenApiRoute,
11
11
  } from "./routes";
12
+ import { parseOpenApiEndpoint } from "./utils";
12
13
  import {
13
14
  getOpenApiOperationDoc,
14
15
  OPENAPI_REQUEST_SECTION_LABELS,
@@ -18,10 +19,19 @@ import {
18
19
  type OpenApiRequestSectionVariantData,
19
20
  } from "./openapi/operation-doc";
20
21
  import { resolvePageDescription } from "./page-description";
21
- import { getConfig } from "./validation";
22
+ import {
23
+ getConfig,
24
+ type DocsConfig,
25
+ type NavGroup,
26
+ type NavMenu,
27
+ type NavOpenApi,
28
+ type NavOpenApiPage,
29
+ type NavPage,
30
+ } from "./validation";
22
31
 
23
32
  const DOCS_ROOT = path.join(process.cwd(), "src/content/docs");
24
33
  const MAX_DESCRIPTION_LENGTH = 300;
34
+ const PRO_TIER = 3;
25
35
 
26
36
  const OPENAPI_SPEC_EXTENSIONS = new Set([".json", ".yaml", ".yml"]);
27
37
 
@@ -29,6 +39,7 @@ export const MARKDOWN_CONTENT_TYPE = "text/markdown; charset=utf-8";
29
39
  export const PLAIN_TEXT_CONTENT_TYPE = "text/plain; charset=utf-8";
30
40
 
31
41
  export type AiMarkdownPage = {
42
+ routeIdentity?: string;
32
43
  filePath: string;
33
44
  routeSlug?: string;
34
45
  title: string;
@@ -90,6 +101,24 @@ function buildPublicUrl(pathname: string): string {
90
101
  return new URL(publicPath, siteOrigin).toString();
91
102
  }
92
103
 
104
+ function getOrgTier(): number {
105
+ const parsed = Number.parseInt((process.env.ORG_TIER ?? "1").trim(), 10);
106
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
107
+ }
108
+
109
+ function formatMcpSection(): string[] {
110
+ if (getOrgTier() < PRO_TIER) return [];
111
+
112
+ return [
113
+ "## MCP",
114
+ "",
115
+ "This documentation exposes a read-only MCP server for AI agents:",
116
+ "",
117
+ `- MCP server: ${buildPublicUrl("/_mcp/server")}`,
118
+ "- Tool: searchDocs(query)",
119
+ ];
120
+ }
121
+
93
122
  function getMarkdownPathForSlug(routeSlug: string): string {
94
123
  const normalizedSlug = routeSlug.replace(/^\/+/, "").replace(/\/+$/, "");
95
124
  return normalizedSlug ? `/${normalizedSlug}.md` : "/index.md";
@@ -142,19 +171,23 @@ async function getMdxEntryByFilePath(filePath: string) {
142
171
  async function createMarkdownPage(args: {
143
172
  filePath: string;
144
173
  routeSlug?: string;
174
+ routeIdentity?: string;
145
175
  title?: string;
146
176
  markdownPath?: string;
147
177
  canonicalPath?: string;
148
178
  }): Promise<AiMarkdownPage> {
149
179
  const entry = await getMdxEntryByFilePath(args.filePath);
150
180
  if (!entry) {
151
- throw new Error(`Could not find content collection entry for "${args.filePath}".`);
181
+ throw new Error(
182
+ `Could not find content collection entry for "${args.filePath}".`,
183
+ );
152
184
  }
153
185
 
154
186
  const { sourcePath, source } = await readMdxSource(args.filePath);
155
187
  const routeSlug = args.routeSlug ?? "";
156
188
  const markdownPath = args.markdownPath ?? getMarkdownPathForSlug(routeSlug);
157
- const canonicalPath = args.canonicalPath ?? getCanonicalPathForSlug(routeSlug);
189
+ const canonicalPath =
190
+ args.canonicalPath ?? getCanonicalPathForSlug(routeSlug);
158
191
  const title =
159
192
  args.title ??
160
193
  resolveMdxPageTitle({
@@ -164,6 +197,7 @@ async function createMarkdownPage(args: {
164
197
  const config = await getConfig();
165
198
 
166
199
  return {
200
+ routeIdentity: args.routeIdentity,
167
201
  filePath: normalizeDocsRootRelativePath(args.filePath),
168
202
  routeSlug,
169
203
  title,
@@ -205,6 +239,7 @@ export async function getLlmsListedMarkdownPages(): Promise<AiMarkdownPage[]> {
205
239
  await createMarkdownPage({
206
240
  filePath: config.home,
207
241
  routeSlug: "",
242
+ routeIdentity: homeRoute?.routeIdentity,
208
243
  title: homeRoute?.title,
209
244
  markdownPath: "/index.md",
210
245
  canonicalPath: "/",
@@ -277,6 +312,234 @@ function formatDocsListItem(page: AiMarkdownPage): string {
277
312
  return `- [${page.title}](${page.markdownUrl})${description}`;
278
313
  }
279
314
 
315
+ type LlmsTxtSection = {
316
+ title: string;
317
+ pages: AiMarkdownPage[];
318
+ };
319
+
320
+ type LlmsNavPageItem = string | NavPage | NavGroup | NavOpenApiPage;
321
+
322
+ type LlmsNavigationContainer = {
323
+ pages?: LlmsNavPageItem[];
324
+ menu?: NavMenu;
325
+ openapi?: string | NavOpenApi;
326
+ };
327
+
328
+ function getAiMarkdownPageKey(page: AiMarkdownPage): string {
329
+ return page.routeIdentity ?? `${page.filePath}:${page.markdownUrl}`;
330
+ }
331
+
332
+ function buildLlmsSectionTitle(parts: string[]): string {
333
+ const title = parts
334
+ .map((part) => part.trim())
335
+ .filter(Boolean)
336
+ .join(": ");
337
+
338
+ return title || "Docs";
339
+ }
340
+
341
+ function isNavPageItem(item: LlmsNavPageItem): item is string | NavPage {
342
+ return typeof item === "string" || "page" in item;
343
+ }
344
+
345
+ function isNavGroupItem(item: LlmsNavPageItem): item is NavGroup {
346
+ return typeof item !== "string" && "group" in item;
347
+ }
348
+
349
+ function isNavOpenApiPageItem(
350
+ item: LlmsNavPageItem,
351
+ ): item is NavOpenApiPage {
352
+ return typeof item !== "string" && "openapi" in item;
353
+ }
354
+
355
+ function getNavPageRouteIdentity(item: string | NavPage): string {
356
+ return `mdx:${typeof item === "string" ? item : item.page}`;
357
+ }
358
+
359
+ function getNavOpenApiPageRouteIdentity(item: NavOpenApiPage): string | null {
360
+ const parsedEndpoint = parseOpenApiEndpoint(item.openapi.endpoint);
361
+ if (!parsedEndpoint) return null;
362
+
363
+ return `openapi:${item.openapi.source}:${parsedEndpoint.method} ${parsedEndpoint.path}`;
364
+ }
365
+
366
+ function getNavOpenApiSource(openapi: string | NavOpenApi): string {
367
+ return typeof openapi === "string" ? openapi : openapi.source;
368
+ }
369
+
370
+ function getOpenApiEndpointKey(method: string, pathStr: string): string {
371
+ return `${method.toUpperCase()} ${pathStr.toLowerCase()}`;
372
+ }
373
+
374
+ function matchesOpenApiEndpointFilter(
375
+ route: OpenApiRoute,
376
+ endpoints: string[],
377
+ ): boolean {
378
+ const routeEndpointKey = getOpenApiEndpointKey(
379
+ route.openApiMethod,
380
+ route.openApiPath,
381
+ );
382
+
383
+ return endpoints.some((endpoint) => {
384
+ const parsedEndpoint = parseOpenApiEndpoint(endpoint);
385
+ if (!parsedEndpoint) return false;
386
+
387
+ return (
388
+ getOpenApiEndpointKey(parsedEndpoint.method, parsedEndpoint.path) ===
389
+ routeEndpointKey
390
+ );
391
+ });
392
+ }
393
+
394
+ function getOpenApiFileRouteIdentities(
395
+ openapi: string | NavOpenApi,
396
+ routes: Route[],
397
+ ): string[] {
398
+ const source = getNavOpenApiSource(openapi);
399
+ const include = typeof openapi === "string" ? undefined : openapi.include;
400
+ const exclude = typeof openapi === "string" ? undefined : openapi.exclude;
401
+
402
+ return routes
403
+ .filter((route): route is OpenApiRoute => {
404
+ if (route.type !== "openapi" || route.filePath !== source) return false;
405
+ if (include && !matchesOpenApiEndpointFilter(route, include)) {
406
+ return false;
407
+ }
408
+ if (exclude && matchesOpenApiEndpointFilter(route, exclude)) {
409
+ return false;
410
+ }
411
+
412
+ return true;
413
+ })
414
+ .map((route) => route.routeIdentity);
415
+ }
416
+
417
+ function buildLlmsTxtSections(args: {
418
+ config: DocsConfig;
419
+ pages: AiMarkdownPage[];
420
+ routes: Route[];
421
+ }): LlmsTxtSection[] {
422
+ const pageByRouteIdentity = new Map(
423
+ args.pages
424
+ .filter((page) => page.routeIdentity)
425
+ .map((page) => [page.routeIdentity as string, page]),
426
+ );
427
+ const emittedPageKeys = new Set<string>();
428
+ const sections: LlmsTxtSection[] = [];
429
+
430
+ const appendSection = (titleParts: string[], routeIdentities: string[]) => {
431
+ const sectionPages: AiMarkdownPage[] = [];
432
+
433
+ for (const routeIdentity of routeIdentities) {
434
+ const page = pageByRouteIdentity.get(routeIdentity);
435
+ if (!page) continue;
436
+
437
+ const pageKey = getAiMarkdownPageKey(page);
438
+ if (emittedPageKeys.has(pageKey)) continue;
439
+
440
+ emittedPageKeys.add(pageKey);
441
+ sectionPages.push(page);
442
+ }
443
+
444
+ if (sectionPages.length === 0) return;
445
+
446
+ const title = buildLlmsSectionTitle(titleParts);
447
+ const existingSection = sections.find((section) => section.title === title);
448
+
449
+ if (existingSection) {
450
+ existingSection.pages.push(...sectionPages);
451
+ return;
452
+ }
453
+
454
+ sections.push({
455
+ title,
456
+ pages: sectionPages,
457
+ });
458
+ };
459
+
460
+ const collectPages = (items: LlmsNavPageItem[], titleParts: string[]) => {
461
+ let directRouteIdentities: string[] = [];
462
+ const flushDirectPages = () => {
463
+ appendSection(titleParts, directRouteIdentities);
464
+ directRouteIdentities = [];
465
+ };
466
+
467
+ for (const item of items) {
468
+ if (isNavPageItem(item)) {
469
+ directRouteIdentities.push(getNavPageRouteIdentity(item));
470
+ continue;
471
+ }
472
+
473
+ if (isNavOpenApiPageItem(item)) {
474
+ const routeIdentity = getNavOpenApiPageRouteIdentity(item);
475
+ if (routeIdentity) directRouteIdentities.push(routeIdentity);
476
+ continue;
477
+ }
478
+
479
+ if (isNavGroupItem(item)) {
480
+ flushDirectPages();
481
+ collectNavigationContainer(
482
+ { pages: item.pages },
483
+ [...titleParts, item.group],
484
+ );
485
+ }
486
+ }
487
+
488
+ flushDirectPages();
489
+ };
490
+
491
+ const collectNavigationContainer = (
492
+ container: LlmsNavigationContainer,
493
+ titleParts: string[],
494
+ ) => {
495
+ if (container.pages) {
496
+ collectPages(container.pages, titleParts);
497
+ }
498
+
499
+ if (container.openapi) {
500
+ appendSection(
501
+ titleParts.length ? titleParts : ["API Reference"],
502
+ getOpenApiFileRouteIdentities(container.openapi, args.routes),
503
+ );
504
+ }
505
+
506
+ if (container.menu) {
507
+ for (const menuItem of container.menu.items) {
508
+ collectNavigationContainer(menuItem, [...titleParts, menuItem.label]);
509
+ }
510
+ }
511
+ };
512
+
513
+ const navigation = args.config.navigation;
514
+
515
+ if (navigation.pages) {
516
+ collectPages(navigation.pages, []);
517
+ } else if (navigation.menu) {
518
+ collectNavigationContainer({ menu: navigation.menu }, []);
519
+ } else if (navigation.openapi) {
520
+ collectNavigationContainer({ openapi: navigation.openapi }, [
521
+ "API Reference",
522
+ ]);
523
+ } else if (navigation.tabs) {
524
+ for (const tabItem of navigation.tabs.items) {
525
+ collectNavigationContainer(tabItem, [tabItem.label]);
526
+ }
527
+ }
528
+
529
+ const remainingPages = args.pages.filter(
530
+ (page) => !emittedPageKeys.has(getAiMarkdownPageKey(page)),
531
+ );
532
+
533
+ if (remainingPages.length > 0) {
534
+ sections.push({
535
+ title: sections.length ? "Additional Docs" : "Docs",
536
+ pages: remainingPages,
537
+ });
538
+ }
539
+
540
+ return sections;
541
+ }
542
+
280
543
  function normalizeLocalOpenApiSource(source: string): string {
281
544
  return normalizeDocsRootRelativePath(source);
282
545
  }
@@ -297,7 +560,9 @@ function getOpenApiSpecLabel(source: string): string {
297
560
  if (isRemoteUrl(source)) {
298
561
  try {
299
562
  const url = new URL(source);
300
- return path.posix.basename(url.pathname).replace(/\.[^.]+$/, "") || source;
563
+ return (
564
+ path.posix.basename(url.pathname).replace(/\.[^.]+$/, "") || source
565
+ );
301
566
  } catch {
302
567
  return source;
303
568
  }
@@ -344,7 +609,9 @@ function createOpenApiSpecArtifact(source: string): OpenApiSpecArtifact | null {
344
609
  };
345
610
  }
346
611
 
347
- export async function getOpenApiSpecArtifacts(): Promise<OpenApiSpecArtifact[]> {
612
+ export async function getOpenApiSpecArtifacts(): Promise<
613
+ OpenApiSpecArtifact[]
614
+ > {
348
615
  const routes = await getAllRoutes();
349
616
  const sources = new Set(
350
617
  routes
@@ -566,10 +833,7 @@ function getResponseContentTypes(response: any): string[] {
566
833
  return Object.keys(content);
567
834
  }
568
835
 
569
- function appendOpenApiResponsesMarkdown(
570
- lines: string[],
571
- responses: unknown,
572
- ) {
836
+ function appendOpenApiResponsesMarkdown(lines: string[], responses: unknown) {
573
837
  if (!responses || typeof responses !== "object") return;
574
838
 
575
839
  const responseEntries = Object.entries(responses).filter(
@@ -662,6 +926,7 @@ async function createOpenApiMarkdownPage(args: {
662
926
  args.canonicalPath ?? getCanonicalPathForSlug(routeSlug);
663
927
 
664
928
  return {
929
+ routeIdentity: args.route.routeIdentity,
665
930
  filePath: normalizeDocsRootRelativePath(args.route.filePath),
666
931
  routeSlug,
667
932
  title: args.route.title,
@@ -690,6 +955,7 @@ async function createRouteMarkdownPage(
690
955
  return createMarkdownPage({
691
956
  filePath: route.filePath,
692
957
  routeSlug: route.slug,
958
+ routeIdentity: route.routeIdentity,
693
959
  title: route.title,
694
960
  ...overrides,
695
961
  });
@@ -729,16 +995,30 @@ export async function getLlmsTxt(): Promise<string> {
729
995
 
730
996
  const config = await getConfig();
731
997
  const pages = await getLlmsListedMarkdownPages();
998
+ const routes = await getAllRoutes();
732
999
  const specs = await getOpenApiSpecArtifacts();
733
- const lines = [`# ${config.title}`, "", "## Docs", ""];
734
-
735
- lines.push(...pages.map(formatDocsListItem));
1000
+ const sections = buildLlmsTxtSections({ config, pages, routes });
1001
+ const lines = [`# ${config.title}`];
1002
+
1003
+ for (const section of sections) {
1004
+ lines.push(
1005
+ "",
1006
+ `## ${section.title}`,
1007
+ "",
1008
+ ...section.pages.map(formatDocsListItem),
1009
+ );
1010
+ }
736
1011
 
737
1012
  const specsSection = formatOpenApiSpecsSection(specs);
738
1013
  if (specsSection.length > 0) {
739
1014
  lines.push("", ...specsSection);
740
1015
  }
741
1016
 
1017
+ const mcpSection = formatMcpSection();
1018
+ if (mcpSection.length > 0) {
1019
+ lines.push("", ...mcpSection);
1020
+ }
1021
+
742
1022
  return `${lines.join("\n").trimEnd()}\n`;
743
1023
  }
744
1024
 
@@ -785,7 +1065,9 @@ export async function getLlmsFullTxt(): Promise<string> {
785
1065
  continue;
786
1066
  }
787
1067
 
788
- sections.push(formatLlmsFullSpec(spec, await readOpenApiSpecArtifact(spec)));
1068
+ sections.push(
1069
+ formatLlmsFullSpec(spec, await readOpenApiSpecArtifact(spec)),
1070
+ );
789
1071
  }
790
1072
 
791
1073
  return `${sections.join("\n\n---\n\n").trimEnd()}\n`;
@@ -3,7 +3,7 @@ import {
3
3
  getMarkdownRoutePages,
4
4
  MARKDOWN_CONTENT_TYPE,
5
5
  type AiMarkdownPage,
6
- } from "../../lib/ai-artifacts";
6
+ } from "../lib/ai-artifacts";
7
7
 
8
8
  export async function getStaticPaths() {
9
9
  const pages = await getMarkdownRoutePages();