@f5-sales-demo/xcsh 22.1.2 → 22.2.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.
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "22.1.2",
21
- "commit": "65d611cfa87b2269f14410aab45331c1701ecf7d",
22
- "shortCommit": "65d611c",
20
+ "version": "22.2.0",
21
+ "commit": "5d9f4f3ce9b3fc191846cfe6fa37d60afab62421",
22
+ "shortCommit": "5d9f4f3",
23
23
  "branch": "main",
24
- "tag": "v22.1.2",
25
- "commitDate": "2026-09-27T23:52:20Z",
26
- "buildDate": "2026-09-28T00:35:39.460Z",
24
+ "tag": "v22.2.0",
25
+ "commitDate": "2026-09-28T01:49:00Z",
26
+ "buildDate": "2026-09-28T02:25:11.763Z",
27
27
  "dirty": true,
28
28
  "prNumber": "",
29
29
  "repoUrl": "https://github.com/f5-sales-demo/xcsh",
30
30
  "repoSlug": "f5-sales-demo/xcsh",
31
- "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/65d611cfa87b2269f14410aab45331c1701ecf7d",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v22.1.2"
31
+ "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/5d9f4f3ce9b3fc191846cfe6fa37d60afab62421",
32
+ "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v22.2.0"
33
33
  };
@@ -0,0 +1,209 @@
1
+ export type KnowledgeRoute = "api" | "documentation" | "direct-uri" | "none";
2
+ export type DocumentationKnowledgeSource = "docs-cloud-f5-com" | "my-f5-com";
3
+
4
+ export interface KnowledgeClassifierResource {
5
+ readonly name: string;
6
+ readonly aliases: readonly string[];
7
+ readonly domain: string;
8
+ readonly categories: readonly string[];
9
+ }
10
+
11
+ export interface KnowledgeSearchPlan {
12
+ readonly intent: string;
13
+ readonly lex: readonly string[];
14
+ readonly vec: readonly string[];
15
+ readonly hyde: readonly string[];
16
+ }
17
+
18
+ export interface KnowledgeConstraints {
19
+ readonly domain?: string;
20
+ readonly source?: DocumentationKnowledgeSource;
21
+ }
22
+
23
+ export interface KnowledgeClassification {
24
+ readonly route: KnowledgeRoute;
25
+ readonly confidence: number;
26
+ readonly query: KnowledgeSearchPlan;
27
+ readonly constraints: KnowledgeConstraints;
28
+ readonly resource?: string;
29
+ readonly domain?: string;
30
+ readonly source?: DocumentationKnowledgeSource;
31
+ readonly documentPath?: string;
32
+ readonly directUri?: string;
33
+ }
34
+
35
+ export interface KnowledgeClassifierOptions {
36
+ readonly toolsEnabled: boolean;
37
+ readonly resources?: readonly KnowledgeClassifierResource[];
38
+ readonly documentationSource?: DocumentationKnowledgeSource;
39
+ readonly previous?: KnowledgeClassification | null;
40
+ }
41
+
42
+ const EMPTY_QUERY: KnowledgeSearchPlan = { intent: "", lex: [], vec: [], hyde: [] };
43
+ const THIRD_PARTY_SCOPE = /\b(?:aws|amazon|azure|gcp|google cloud|kubernetes)\b/i;
44
+ const F5_SCOPE = /\b(?:f5(?:\s+distributed\s+cloud)?|xc)\b/i;
45
+ const ANAPHORIC = /\b(?:its|it|that|this|those|them|the same)\b/i;
46
+ const API_METADATA =
47
+ /\b(?:api|endpoint|operation\s*id|api\s+path|http\s+method|method|payload|request\s+body|required\s+fields?|enum|allowed\s+values?|constraints?|limits?|maximum|max(?:imum)?|minimum|min(?:imum)?|how\s+many|create|creates|creating|get|gets|list|lists|update|updates|replace|replaces|delete|deletes|clone|import)\b/i;
48
+ const DOCUMENTATION_INTENT =
49
+ /\b(?:documentation|docs?|guide|tutorial|how\s+(?:do|can|to)|configure|configuration|set\s*up|troubleshoot|procedure|instructions?)\b/i;
50
+ const EXCLUDED_API_INTENT = /\b(?:pricing|price|quote|licen[cs]ing|sales)\b/i;
51
+ const DIRECT_URI = /\bxcsh:\/\/(?:api-catalog|api-spec|documentation)\/[^\s)>\]}]*/i;
52
+
53
+ function normalizePhrase(value: string): string {
54
+ return value
55
+ .toLocaleLowerCase("en-US")
56
+ .replace(/[_-]+/g, " ")
57
+ .replace(/[^a-z0-9]+/g, " ")
58
+ .trim()
59
+ .replace(/\s+/g, " ");
60
+ }
61
+
62
+ function action(value: string): { base?: string; gerund?: string } {
63
+ if (/\b(?:create|creates|creating)\b/i.test(value)) return { base: "create", gerund: "creating" };
64
+ if (/\b(?:configure|configuration|configuring)\b/i.test(value)) return { base: "configure", gerund: "configuring" };
65
+ if (/\b(?:set\s*up|setting\s*up)\b/i.test(value)) return { base: "set up", gerund: "setting up" };
66
+ if (/\b(?:get|gets|read|fetch)\b/i.test(value)) return { base: "get", gerund: "getting" };
67
+ if (/\b(?:list|lists|listing)\b/i.test(value)) return { base: "list", gerund: "listing" };
68
+ if (/\b(?:update|updates|updating)\b/i.test(value)) return { base: "update", gerund: "updating" };
69
+ if (/\b(?:replace|replaces|replacing)\b/i.test(value)) return { base: "replace", gerund: "replacing" };
70
+ if (/\b(?:delete|deletes|remove|removing)\b/i.test(value)) return { base: "delete", gerund: "deleting" };
71
+ if (/\bclone\b/i.test(value)) return { base: "clone", gerund: "cloning" };
72
+ if (/\bimport\b/i.test(value)) return { base: "import", gerund: "importing" };
73
+ return {};
74
+ }
75
+
76
+ function documentationSubject(prompt: string): string {
77
+ const normalized = normalizePhrase(prompt);
78
+ const withoutPreamble = normalized
79
+ .replace(/^(?:please )?(?:use|read|find|show me|show) /, "")
80
+ .replace(/^(?:the )?(?:official |pinned )?f5 (?:distributed cloud )?(?:documentation|docs?) (?:to |for )?/, "")
81
+ .replace(/^(?:the )?(?:official |pinned )?(?:documentation|docs?) (?:to |for )?/, "");
82
+ return (withoutPreamble || normalized)
83
+ .replace(/\b(?:a|an|the)\b/g, " ")
84
+ .replace(/\s+/g, " ")
85
+ .trim();
86
+ }
87
+
88
+ export function buildKnowledgeSearchPlan(route: "api" | "documentation", subject: string): KnowledgeSearchPlan {
89
+ const normalized = normalizePhrase(subject);
90
+ const verb = action(normalized);
91
+ if (route === "api") {
92
+ const resource = verb.base
93
+ ? normalizePhrase(normalized.replace(new RegExp(`\\b${verb.base}\\b`), ""))
94
+ : normalized;
95
+ const lex = [...new Set([resource, verb.base ? `${verb.base} ${resource}` : resource].filter(Boolean))];
96
+ const phrase = verb.gerund ? `${verb.gerund} ${resource}` : resource;
97
+ return {
98
+ intent: `Find the authoritative F5 Distributed Cloud API operation for ${phrase}.`,
99
+ lex,
100
+ vec: [`${verb.base ? `${verb.base} ` : ""}${resource} F5 Distributed Cloud API`],
101
+ hyde: [
102
+ `An authoritative F5 Distributed Cloud API operation for ${phrase}, including method, path, operation ID, request fields, and constraints.`,
103
+ ],
104
+ };
105
+ }
106
+ const phrase = verb.gerund ? normalized.replace(new RegExp(`^${verb.base}`), verb.gerund) : normalized;
107
+ return {
108
+ intent: `Find the authoritative F5 documentation page for ${phrase}.`,
109
+ lex: [normalized],
110
+ vec: [`${subject.trim()} F5 documentation`],
111
+ hyde: [
112
+ `An authoritative F5 documentation page explains how to ${subject.trim()} with prerequisites, ordered steps, verification, and limitations.`,
113
+ ],
114
+ };
115
+ }
116
+
117
+ function none(): KnowledgeClassification {
118
+ return { route: "none", confidence: 0, query: EMPTY_QUERY, constraints: {} };
119
+ }
120
+
121
+ function direct(prompt: string): KnowledgeClassification | null {
122
+ const directUri = DIRECT_URI.exec(prompt)?.[0];
123
+ if (!directUri) return null;
124
+ try {
125
+ const parsed = new URL(directUri);
126
+ const constraints: KnowledgeConstraints = {};
127
+ if (parsed.hostname === "api-spec") {
128
+ const domain = parsed.pathname.split("/").filter(Boolean)[0];
129
+ if (domain) Object.assign(constraints, { domain });
130
+ }
131
+ if (parsed.hostname === "documentation") {
132
+ const source = parsed.pathname.split("/").filter(Boolean)[0];
133
+ if (source === "docs-cloud-f5-com" || source === "my-f5-com") Object.assign(constraints, { source });
134
+ }
135
+ return { route: "direct-uri", confidence: 1, directUri, constraints, query: EMPTY_QUERY };
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+
141
+ function selectResource(
142
+ prompt: string,
143
+ resources: readonly KnowledgeClassifierResource[],
144
+ ): KnowledgeClassifierResource | undefined {
145
+ const normalizedPrompt = ` ${normalizePhrase(prompt)} `;
146
+ return resources
147
+ .flatMap(resource =>
148
+ resource.aliases
149
+ .map(alias => normalizePhrase(alias))
150
+ .filter(alias => alias && normalizedPrompt.includes(` ${alias} `))
151
+ .map(alias => ({ resource, alias })),
152
+ )
153
+ .sort(
154
+ (left, right) =>
155
+ right.alias.length - left.alias.length ||
156
+ right.resource.categories.length - left.resource.categories.length ||
157
+ left.resource.name.localeCompare(right.resource.name) ||
158
+ left.resource.domain.localeCompare(right.resource.domain),
159
+ )[0]?.resource;
160
+ }
161
+
162
+ export function classifyKnowledgeRequest(prompt: string, options: KnowledgeClassifierOptions): KnowledgeClassification {
163
+ if (!options.toolsEnabled) return none();
164
+ const directResult = direct(prompt);
165
+ if (directResult) return directResult;
166
+
167
+ const resources = options.resources ?? [];
168
+ const selected = selectResource(prompt, resources);
169
+ const thirdPartyOnly = THIRD_PARTY_SCOPE.test(prompt) && !F5_SCOPE.test(prompt);
170
+ if (!thirdPartyOnly && !EXCLUDED_API_INTENT.test(prompt) && API_METADATA.test(prompt)) {
171
+ const contextual =
172
+ !selected && ANAPHORIC.test(prompt) && options.previous?.route === "api" ? options.previous : null;
173
+ const resource = selected?.name ?? contextual?.resource;
174
+ const domain = selected?.domain ?? contextual?.domain;
175
+ if (resource && domain) {
176
+ const alias = normalizePhrase(selected?.aliases[0] ?? contextual?.query.lex[0] ?? resource);
177
+ const verb = action(prompt).base;
178
+ const subject = verb ? `${verb} ${alias}` : alias;
179
+ return {
180
+ route: "api",
181
+ confidence: F5_SCOPE.test(prompt) || contextual ? 0.99 : 0.97,
182
+ resource,
183
+ domain,
184
+ constraints: { domain },
185
+ query: buildKnowledgeSearchPlan("api", subject),
186
+ };
187
+ }
188
+ }
189
+
190
+ const previousDocumentation = options.previous?.route === "documentation" ? options.previous : null;
191
+ if (
192
+ !thirdPartyOnly &&
193
+ (DOCUMENTATION_INTENT.test(prompt) || (ANAPHORIC.test(prompt) && previousDocumentation)) &&
194
+ (F5_SCOPE.test(prompt) || /\b(?:documentation|docs?)\b/i.test(prompt) || previousDocumentation)
195
+ ) {
196
+ const source = options.documentationSource ?? previousDocumentation?.source;
197
+ const subject = documentationSubject(prompt);
198
+ return {
199
+ route: "documentation",
200
+ confidence: /\b(?:documentation|docs?)\b/i.test(prompt) ? 0.99 : 0.96,
201
+ source,
202
+ documentPath: previousDocumentation?.documentPath,
203
+ constraints: source ? { source } : {},
204
+ query: buildKnowledgeSearchPlan("documentation", subject),
205
+ };
206
+ }
207
+
208
+ return none();
209
+ }
@@ -0,0 +1,90 @@
1
+ import type { KnowledgeSearchPlan } from "./knowledge-classifier";
2
+
3
+ export type KnowledgeSearchMode = "bm25" | "hybrid";
4
+ export type KnowledgeSearchCorpus = "api" | "documentation";
5
+
6
+ export interface KnowledgeSearchResult {
7
+ readonly displayPath: string;
8
+ readonly score: number;
9
+ readonly [key: string]: unknown;
10
+ }
11
+
12
+ export interface KnowledgeSearchStore<T extends KnowledgeSearchResult = KnowledgeSearchResult> {
13
+ search(options: Record<string, unknown>): Promise<readonly T[]>;
14
+ searchLex(query: string, options?: { limit?: number; collection?: string | string[] }): Promise<readonly T[]>;
15
+ }
16
+
17
+ export interface KnowledgeSearchOptions {
18
+ readonly mode: KnowledgeSearchMode;
19
+ readonly corpus: KnowledgeSearchCorpus;
20
+ readonly collections?: readonly string[];
21
+ readonly limit: number;
22
+ readonly candidateLimit?: number;
23
+ readonly rerank?: boolean;
24
+ readonly explain?: boolean;
25
+ readonly minScore?: number;
26
+ }
27
+
28
+ export const KNOWLEDGE_SCORE_THRESHOLDS = {
29
+ api: 0.2,
30
+ documentation: 0.25,
31
+ } as const;
32
+
33
+ function interleave<T extends KnowledgeSearchResult>(lists: readonly (readonly T[])[], limit: number): T[] {
34
+ const results: T[] = [];
35
+ const seen = new Set<string>();
36
+ for (let position = 0; results.length < limit; position++) {
37
+ let found = false;
38
+ for (const list of lists) {
39
+ const result = list[position];
40
+ if (!result) continue;
41
+ found = true;
42
+ if (!seen.has(result.displayPath)) {
43
+ seen.add(result.displayPath);
44
+ results.push(result);
45
+ if (results.length === limit) break;
46
+ }
47
+ }
48
+ if (!found) break;
49
+ }
50
+ return results;
51
+ }
52
+
53
+ /**
54
+ * Runs a deterministic structured query. The BM25 mode never invokes model
55
+ * APIs; hybrid mode supplies typed queries so QMD never loads its expansion
56
+ * model. Native/model errors deliberately propagate to the caller.
57
+ */
58
+ export async function searchKnowledgeStore<T extends KnowledgeSearchResult>(
59
+ store: KnowledgeSearchStore<T>,
60
+ plan: KnowledgeSearchPlan,
61
+ options: KnowledgeSearchOptions,
62
+ ): Promise<readonly T[]> {
63
+ if (options.mode === "bm25") {
64
+ const lists: Array<readonly T[]> = [];
65
+ for (const query of plan.lex) {
66
+ lists.push(
67
+ await store.searchLex(query, {
68
+ limit: options.limit,
69
+ collection: options.collections ? [...options.collections] : undefined,
70
+ }),
71
+ );
72
+ }
73
+ return interleave(lists, options.limit);
74
+ }
75
+
76
+ return store.search({
77
+ queries: [
78
+ ...plan.lex.map(query => ({ type: "lex" as const, query })),
79
+ ...plan.vec.map(query => ({ type: "vec" as const, query })),
80
+ ...plan.hyde.map(query => ({ type: "hyde" as const, query })),
81
+ ],
82
+ intent: plan.intent,
83
+ collections: options.collections ? [...options.collections] : undefined,
84
+ limit: options.limit,
85
+ candidateLimit: options.candidateLimit ?? 30,
86
+ minScore: options.minScore ?? KNOWLEDGE_SCORE_THRESHOLDS[options.corpus],
87
+ explain: options.explain ?? true,
88
+ rerank: options.rerank ?? false,
89
+ });
90
+ }
@@ -29,7 +29,7 @@ This document is the authoritative answer when the user asks about xcsh itself.
29
29
 
30
30
  xcsh serves F5 Distributed Cloud sales engineers. Product documentation is
31
31
  federated across the f5-sales-demo GitHub organization. Entry point:
32
- https://f5-sales-demo.github.io/docs/llms.txt
32
+ https://f5-sales-demo.github.io/llms.txt
33
33
 
34
34
  Each product repo publishes: llms.txt (index with sidebar nav), custom sets
35
35
  at /_llms-txt/{topic}.txt, per-page content at /{slug}.md, plus
@@ -5,6 +5,15 @@ Select the best result and read its exact follow-up URL,
5
5
  `xcsh://documentation/<source>/<stable-path>/index.md`, before answering. Use the source filter when the question
6
6
  clearly targets `docs-cloud-f5-com` or `my-f5-com`.
7
7
 
8
+ Render documentation images only when the user explicitly asks to see them. Extract all unique Markdown image references
9
+ from the exact document, resolve relative references against its exact `xcsh://documentation/` URI, and
10
+ read each asset one at a time in document order. Static screenshot collections are not timelines or slideshows.
11
+ Pair every returned image with its official Markdown caption verbatim and one conservative explanation grounded in
12
+ the image, its caption, and nearby prose. If pixels are unavailable, use only the caption and nearby prose without
13
+ inventing visual details. Do not announce or estimate the image count before all reads finish. Afterward, verify that
14
+ the reported image count matches the number of successful asset reads. Documentation assets use `read`, even when
15
+ generic image-inspection guidance prefers `inspect_image`.
16
+
8
17
  For API paths, methods, payload fields, required status, enums, and constraints, use the deterministic
9
18
  `api-catalog-preflight` result followed by exact `xcsh://api-catalog/` and `xcsh://api-spec/` reads first.
10
19
  Do not route API metadata through the documentation QMD index.
@@ -13,7 +22,7 @@ Use the live cascade below only when the pinned documentation is stale or missin
13
22
  the QMD lookup fails. You **MUST** disclose why the pinned snapshot is stale, missing, or unavailable before the
14
23
  first live lookup:
15
24
 
16
- 1. **Federation index** — Read `https://f5-sales-demo.github.io/docs/llms.txt` and select the relevant categorized site.
25
+ 1. **Federation index** — Read `https://f5-sales-demo.github.io/llms.txt` and select the relevant categorized site.
17
26
  2. **Site index** — Read that site's `llms.txt`; use its Documentation Sets, Sections, and Translations links as published.
18
27
  3. **Locale index** — Prefer the user's published locale. The default locale's Section normally points to `/_llms-txt/en.txt`; locale-aware tiered paths use `/_llms-txt/{locale}/…`. If a localized endpoint is absent or contains only a system marker, fall back to English and disclose that fallback.
19
28
  4. **Focused content** — Follow `## Contents` links recursively until the narrowest leaf `.txt` answers the question. Generated `/_llms-txt/` links are canonical; do not rewrite them. A same-locale page endpoint such as `/{locale}/{slug}.md` is an equivalent leaf when a page URL is already known.
@@ -495,10 +495,23 @@ search `xcsh://documentation/?search=<query>` first, then read the exact
495
495
  `xcsh://documentation/<source>/<stable-path>/index.md` result before answering. The search result
496
496
  is discovery; the exact Markdown document is the evidence.
497
497
 
498
+ ### Documentation images
499
+
500
+ Render documentation images only when the user explicitly asks to see them. After that request, extract every
501
+ unique Markdown image reference from the exact document, resolve each relative reference against that document's
502
+ `xcsh://documentation/` URI, and preserve document order. Read each exact asset URI sequentially, one at a time, so
503
+ the transcript order is stable. Do not use `display_media` for documentation assets, and do not convert a static
504
+ screenshot collection into a timeline or slideshow.
505
+
506
+ Inspect each returned image conservatively. Present its official Markdown caption verbatim with one grounded explanation.
507
+ If image pixels are unavailable to the active model, quietly rely only on the official caption and nearby document
508
+ prose; never invent visual details. Do not announce or estimate the image count before all reads finish. Afterward,
509
+ verify that the reported image count matches the number of successful asset reads.
510
+
498
511
  If the pinned snapshot is stale, missing, or the QMD lookup fails, disclose that condition before using the live
499
512
  fallback knowledge index:
500
513
 
501
- `https://f5-sales-demo.github.io/docs/llms.txt`
514
+ `https://f5-sales-demo.github.io/llms.txt`
502
515
 
503
516
  Follow links from there to the relevant site's own `llms.txt`, then fetch only the
504
517
  narrowest published content that answers the question. Do not silently replace or supplement a successful
@@ -714,6 +727,7 @@ Don't open a file hoping. Hope is not a strategy.
714
727
  - For image understanding tasks: **MUST** use `inspect_image` over `read` to avoid overloading main session context.
715
728
  - Write a specific `question` for `inspect_image`: what to inspect, constraints (for example verbatim OCR), and desired output format.
716
729
  - If you encounter `[Image content detected but current model does not support vision]` in a message, use `inspect_image` with the image file path to analyze it. Do not ask the user to describe the image — analyze it yourself via the tool.
730
+ - Documentation images are the exception: use the exact `xcsh://documentation/` asset URI with `read`; do not pass internal documentation URIs to `inspect_image`.
717
731
  {{/if}}
718
732
  {{#ifAll (includes tools "inspect_image") (includes tools "generate_image")}}
719
733
  ### Image generation and analysis
@@ -6,3 +6,7 @@ Pass exactly one of:
6
6
  - `frames`: ordered text frames or raster-frame sources with per-frame `durationMs`.
7
7
 
8
8
  Playback is always muted. Autoplay defaults to once, looping is opt-in, and playback is capped at 12 FPS unless a lower or higher cap (up to 60) is requested. Unsupported terminals and missing FFmpeg receive a static poster or text fallback.
9
+
10
+ Do not use this tool for documentation assets. Resolve each Markdown image to its exact
11
+ `xcsh://documentation/<source>/<stable-path>/assets/<filename>` URI and use `read` sequentially. Static screenshot
12
+ collections are not timelines or slideshows.
@@ -21,7 +21,6 @@ export interface LlmsIndex {
21
21
 
22
22
  const ENTRY_PATTERN = /^- \[([^\]]+)\]\(([^)]+)\):\s*(.+)$/;
23
23
  const ROOT_LLMS_FILE = "llms.txt";
24
- const PORTAL_SLUG = "docs";
25
24
 
26
25
  function extractRootLlmsSlug(url: string): string | null {
27
26
  try {
@@ -64,8 +63,9 @@ function isValidIndex(value: unknown): value is LlmsIndex {
64
63
  *
65
64
  * Federation category headings are intentionally open-ended. A list entry is a
66
65
  * topic when it links to a sibling site's root /{slug}/llms.txt endpoint; this
67
- * excludes documentation sets, locale indexes, and tiered content without
68
- * hardcoding today's category names.
66
+ * excludes documentation sets and tiered content. The root portal now has
67
+ * locale feeds at /{locale}/llms.txt, so translation entries are excluded
68
+ * by their heading without restricting future federation categories.
69
69
  */
70
70
  export function parseLlmsTxt(content: string, now?: Date): LlmsIndex {
71
71
  const lines = content.split("\n");
@@ -94,11 +94,11 @@ export function parseLlmsTxt(content: string, now?: Date): LlmsIndex {
94
94
  }
95
95
 
96
96
  const match = ENTRY_PATTERN.exec(trimmed);
97
- if (!match || !category) continue;
97
+ if (!match || !category || category === "Translations") continue;
98
98
 
99
99
  const [, name, url, topicDescription] = match;
100
100
  const slug = extractRootLlmsSlug(url);
101
- if (!slug || slug === PORTAL_SLUG || seenUrls.has(url)) continue;
101
+ if (!slug || seenUrls.has(url)) continue;
102
102
 
103
103
  seenUrls.add(url);
104
104
  topics.push({ name, description: topicDescription, url, category });
@@ -113,7 +113,7 @@ export function parseLlmsTxt(content: string, now?: Date): LlmsIndex {
113
113
  };
114
114
  }
115
115
 
116
- const ROOT_LLMS_URL = "https://f5-sales-demo.github.io/docs/llms.txt";
116
+ const ROOT_LLMS_URL = "https://f5-sales-demo.github.io/llms.txt";
117
117
  const DEFAULT_TTL_MS = 3_600_000;
118
118
 
119
119
  export class KnowledgeService {