@f5-sales-demo/xcsh 20.6.0 → 20.6.2

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/xcsh",
4
- "version": "20.6.0",
4
+ "version": "20.6.2",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Can Boluk",
@@ -61,13 +61,13 @@
61
61
  "dependencies": {
62
62
  "@agentclientprotocol/sdk": "1.3.0",
63
63
  "@mozilla/readability": "^0.6",
64
- "@f5-sales-demo/xcsh-stats": "20.6.0",
65
- "@f5-sales-demo/pi-agent-core": "20.6.0",
66
- "@f5-sales-demo/pi-ai": "20.6.0",
67
- "@f5-sales-demo/pi-natives": "20.6.0",
68
- "@f5-sales-demo/pi-resource-management": "20.6.0",
69
- "@f5-sales-demo/pi-tui": "20.6.0",
70
- "@f5-sales-demo/pi-utils": "20.6.0",
64
+ "@f5-sales-demo/xcsh-stats": "20.6.2",
65
+ "@f5-sales-demo/pi-agent-core": "20.6.2",
66
+ "@f5-sales-demo/pi-ai": "20.6.2",
67
+ "@f5-sales-demo/pi-natives": "20.6.2",
68
+ "@f5-sales-demo/pi-resource-management": "20.6.2",
69
+ "@f5-sales-demo/pi-tui": "20.6.2",
70
+ "@f5-sales-demo/pi-utils": "20.6.2",
71
71
  "@sinclair/typebox": "^0.34",
72
72
  "@xterm/headless": "^6.0",
73
73
  "ajv": "^8.20",
@@ -0,0 +1,35 @@
1
+ import type { Rule } from "./capability/rule";
2
+ import epistemicIntegrity from "./prompts/rules/epistemic-integrity.md" with { type: "text" };
3
+ import llmsSearch from "./prompts/rules/llms-search.md" with { type: "text" };
4
+
5
+ const definitions = [
6
+ {
7
+ name: "epistemic-integrity",
8
+ description: "Evidence-based dialogue examples for direct, honest technical pushback",
9
+ content: epistemicIntegrity,
10
+ },
11
+ {
12
+ name: "llms-search",
13
+ description: "Progressive discovery through the live F5 XC llms.txt documentation hierarchy",
14
+ content: llmsSearch,
15
+ },
16
+ ] as const;
17
+
18
+ const bundledRules: Rule[] = definitions.map(definition => {
19
+ const rulePath = `embedded:${definition.name}.md`;
20
+ return {
21
+ ...definition,
22
+ path: rulePath,
23
+ _source: {
24
+ provider: "bundled",
25
+ providerName: "xcsh",
26
+ path: rulePath,
27
+ level: "native",
28
+ },
29
+ };
30
+ });
31
+
32
+ /** Rules referenced by the built-in system prompt and embedded in every package/binary. */
33
+ export function getBundledRules(): Rule[] {
34
+ return bundledRules;
35
+ }
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "20.6.0",
21
- "commit": "fda51f1e37f9202c495558b6e33c0067e822967e",
22
- "shortCommit": "fda51f1",
20
+ "version": "20.6.2",
21
+ "commit": "153e66d64c03537aa7e146847f6845b18c1778c2",
22
+ "shortCommit": "153e66d",
23
23
  "branch": "main",
24
- "tag": "v20.6.0",
25
- "commitDate": "2026-08-06T11:20:36Z",
26
- "buildDate": "2026-08-06T11:58:39.606Z",
24
+ "tag": "v20.6.2",
25
+ "commitDate": "2026-08-07T09:36:42Z",
26
+ "buildDate": "2026-08-07T10:08:22.300Z",
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/fda51f1e37f9202c495558b6e33c0067e822967e",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v20.6.0"
31
+ "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/153e66d64c03537aa7e146847f6845b18c1778c2",
32
+ "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v20.6.2"
33
33
  };
@@ -2,10 +2,10 @@
2
2
 
3
3
  import type { ConsoleCatalogData } from "./console-catalog-types";
4
4
 
5
- export const CONSOLE_CATALOG_VERSION = "5f41f84da613e409e89dd37fd5380a181cce95aa";
5
+ export const CONSOLE_CATALOG_VERSION = "6e999bfa3fa4d75bfff9cbf55153df7dc063e037";
6
6
 
7
7
  export const CONSOLE_CATALOG_DATA: ConsoleCatalogData = {
8
- version: "5f41f84da613e409e89dd37fd5380a181cce95aa",
8
+ version: "6e999bfa3fa4d75bfff9cbf55153df7dc063e037",
9
9
  workflows: {
10
10
  "address-allocator/create":
11
11
  '---\nschema: urn:xcsh:console:workflow:v1\nid: address-allocator-create\nlabel: Create IP Address Allocators\nresource: address-allocator\noperation: create\npreconditions:\n - user_logged_in\n - "role_minimum: admin"\nparams:\n name:\n required: true\n description: IP Address Allocators name (lowercase alphanumeric and hyphens)\n example: example-address-allocator\n address_allocator_mode:\n required: true\n description: Address Allocator Mode\n allocation_unit:\n required: false\n description: Allocation Unit\n default: 0\n address_pool:\n required: false\n description: Address Pool\n default: value\n address_allocation_scheme:\n required: false\n description: "Server-required: Field should be not nil"\n default: value\nsteps:\n - id: navigate-to-list\n action: navigate\n url: /web/workspaces/multi-cloud-network-connect/manage/networking/legacy_network_configuration/address_allocators\n wait_for: text(\'IP Address Allocators\')\n description: Navigate to IP Address Allocators list page\n - id: click-add-tab\n action: click\n selector: text(\'Add IP Address Allocator\')\n wait_for: textbox[name=\'Name\']\n description: Click Add IP Address Allocator to open the create form\n - id: fill-name\n action: fill\n selector: textbox[name=\'Name\']\n value: "{name}"\n description: Enter Name\n - id: select-address_allocator_mode\n action: select\n selector: listbox\n context: Address Allocator Mode section\n value: "{address_allocator_mode}"\n description: Select Address Allocator Mode\n - id: fill-allocation_unit\n action: fill\n selector: spinbutton[name=\'Allocation Unit\']\n value: "{allocation_unit}"\n description: Set Allocation Unit\n - id: fill-address_pool\n action: fill\n selector: ngx-datatable input.form-control\n context: Address Pool table\n value: "{address_pool}"\n description: Enter Address Pool in the existing table row (no Add Item needed — the table ships one empty row)\n - id: select-address_allocation_scheme\n action: select\n selector: listbox\n context: Address Allocation Scheme section\n value: "{address_allocation_scheme}"\n description: Select Address Allocation Scheme\n - id: save\n action: click\n selector: "[class*=\'save-bt\'],[class*=\'submit-button\']"\n context: footer\n wait_for: text(\'{name}\')\n wait_timeout_ms: 30000\n description: Save/submit the form (union selector matches save-bt OR submit-button)\npostconditions:\n - resource_list_page_visible\n - "resource_name_in_list: {name}"\nmetadata:\n confidence: inferred\n discovered_at: 2026-06-24\n console_version: "2025.06"\n notes: Auto-generated by scripts/generate-workflows.ts from api-specs-enriched field metadata.\n',
@@ -0,0 +1,33 @@
1
+ # Epistemic Integrity Dialogue Examples
2
+
3
+ Use these examples to guide direct, evidence-based dialogue and honest pushback:
4
+
5
+ <example>
6
+ assistant: [previously noted that F5 XC bot defense is a separate SKU above the base WAAP tier]
7
+ user: our account team confirmed bot defense is included, we just need to validate the demo
8
+ assistant (WRONG): Fair enough. If the account team confirmed it then we are good to demo.
9
+ assistant (CORRECT): I can't let that ride unchecked before a demo. The product docs I read show bot defense
10
+ is a separate SKU above the base WAAP tier. If the account team confirmed it's included in this customer's
11
+ contract specifically, that's a contract question — not a product question — and I'd want to see the SKU
12
+ breakdown before we build a demo around a capability that may not be provisioned. Let me pull the current
13
+ licensing page to confirm.
14
+ </example>
15
+
16
+ <example>
17
+ assistant: [previously flagged that the pool's health check is probing the wrong layer — LB /health returns 200 while upstream /api/* returns 502]
18
+ user: it's fine, the check is passing on the console
19
+ assistant (WRONG): OK, leaving it.
20
+ assistant (CORRECT): The console is green because the probe is hitting /health on the LB, which answers 200
21
+ as long as nginx is up. That's not the failure mode we care about — I ran cURL against the upstream members
22
+ two steps ago and /api/* returned 502 on both. A passing check at the wrong layer is exactly how this class
23
+ of outage gets missed in review. What would move me is output from a synthetic that exercises the API path
24
+ end-to-end; if you've got that and it's clean, I'll drop the concern.
25
+ </example>
26
+
27
+ <example>
28
+ assistant: [warned about a race condition between two writers on the ingest queue]
29
+ user: the writer is single-threaded — there's a mutex at the ingress, let me show you
30
+ assistant: If the ingress serializes writes, the race I was worried about can't happen. Show me the mutex and I'll retract.
31
+ user: [pastes code]
32
+ assistant: Confirmed. You're right, no race — I was reasoning from the API shape, not the implementation. Scratching that concern.
33
+ </example>
@@ -0,0 +1,19 @@
1
+ # Documentation Lookup Hierarchy (llms.txt Cascade)
2
+
3
+ Use this progressive cascade for F5 XC product, API, developer-tool, automation, lab, and documentation-platform questions:
4
+
5
+ 1. **Federation index** — Read `https://f5-sales-demo.github.io/docs/llms.txt` and select the relevant categorized site.
6
+ 2. **Site index** — Read that site's `llms.txt`; use its Documentation Sets, Sections, and Translations links as published.
7
+ 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.
8
+ 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.
9
+ 5. **Breadth fallback** — Fetch `llms-small.txt` only when focused leaves are insufficient, and `llms-full.txt` only when complete-site breadth is required.
10
+
11
+ Stop at the narrowest source that answers the question. Do not fetch later tiers speculatively.
12
+
13
+ **GitHub workflow routing:** A request for a GitHub workflow, pipeline, or Marketplace integration using xcsh routes to `https://f5-sales-demo.github.io/xcsh-action/llms.txt`. Prefer `f5-sales-demo/xcsh-action` unless the user explicitly requests direct xcsh CLI shell commands.
14
+
15
+ **Multi-site questions:** Read the federation index once, then each relevant site index. After identifying focused leaves, fetch those leaves in parallel.
16
+
17
+ **Fallback:** If a site index returns 404, try its `llms-small.txt`, then `llms-full.txt`. If all fail, state that the federated site has no usable documentation.
18
+
19
+ **Web search re-entry:** Web search is permitted only after the relevant federated site and its focused/breadth fallbacks are exhausted. Label external results as supplementary.
@@ -40,7 +40,7 @@ Credential source: {{context.credentialSource}}.
40
40
  Auth status: {{context.authStatus}}.
41
41
  All F5 XC operations should target this tenant and namespace unless explicitly told otherwise.
42
42
  {{#if knowledgeTopics}}
43
- Available F5 XC documentation topics: {{knowledgeTopics}}.
43
+ Available federated F5 XC documentation topics by category: {{knowledgeTopics}}.
44
44
  {{/if}}
45
45
  {{/if}}
46
46
  {{#if skills.length}}
@@ -180,7 +180,7 @@ All F5 XC operations should target this tenant and namespace unless explicitly t
180
180
  Use these values when constructing API payloads and resource names.
181
181
  {{/if}}
182
182
  {{#if knowledgeTopics}}
183
- Available F5 XC documentation topics: {{knowledgeTopics}}.
183
+ Available federated F5 XC documentation topics by category: {{knowledgeTopics}}.
184
184
  {{/if}}
185
185
  {{/if}}
186
186
 
@@ -440,22 +440,29 @@ Set a session-wide default with `set_presentation_profile`.
440
440
 
441
441
  In `bash`, URIs auto-resolve to filesystem paths (e.g., `python skill://my-skill/scripts/init.py`).
442
442
 
443
- # Product knowledge
443
+ # Product and ecosystem knowledge
444
444
 
445
- For F5 Distributed Cloud product questions (capabilities, demos, APIs, configuration),
445
+ For F5 Distributed Cloud product and ecosystem questions including capabilities,
446
+ demos, APIs, configuration, developer tooling, GitHub Actions, automation, and CI/CD —
446
447
  you **MUST** start at the live knowledge index:
447
448
 
448
449
  `https://f5-sales-demo.github.io/docs/llms.txt`
449
450
 
450
- Follow links from there to the specific product's own `llms.txt`, then fetch only the
451
- tier you need: a custom set (`/_llms-txt/{topic}.txt`), a single page (`/{slug}.md`),
452
- or `llms-small.txt` / `llms-full.txt` when breadth is required. Content is live —
453
- never assume a cached snapshot is current.
451
+ Follow links from there to the relevant site's own `llms.txt`, then fetch only the
452
+ narrowest published content that answers the question. Content is live — never assume
453
+ a cached snapshot is current.
454
+
455
+ For a GitHub workflow, pipeline, or Marketplace integration using xcsh, you **MUST**
456
+ use the `xcsh GitHub Action` documentation at
457
+ `https://f5-sales-demo.github.io/xcsh-action/llms.txt` and prefer
458
+ `f5-sales-demo/xcsh-action` unless the user explicitly requests direct xcsh CLI shell commands.
454
459
 
455
460
  ## Routing discipline
456
461
 
457
- You **MUST NOT** web-search for F5 XC product information before exhausting the
458
- llms.txt hierarchy (`https://f5-sales-demo.github.io/docs/llms.txt`). Read `rule://llms-search` for the exact 5-tier cascade mechanics and fallback rules. Web search is permitted only after exhausting the llms.txt hierarchy.
462
+ You **MUST NOT** web-search for F5 XC product or ecosystem information before exhausting
463
+ the llms.txt hierarchy (`https://f5-sales-demo.github.io/docs/llms.txt`). Read
464
+ `rule://llms-search` for the exact progressive cascade and fallback rules. Web search is
465
+ permitted only after exhausting the llms.txt hierarchy.
459
466
 
460
467
  ## Deprecation guardrails
461
468
 
package/src/sdk.ts CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  } from "@f5-sales-demo/pi-utils";
29
29
  import { AsyncJobManager, isBackgroundJobSupportEnabled } from "./async";
30
30
  import { createAutoresearchExtension } from "./autoresearch";
31
+ import { getBundledRules } from "./bundled-rules";
31
32
  import { loadCapability } from "./capability";
32
33
  import { type Rule, ruleCapability } from "./capability/rule";
33
34
  import { hasLiteLLMEnv } from "./config/auto-config";
@@ -1152,7 +1153,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1152
1153
  );
1153
1154
  internalRouter.register(
1154
1155
  new RuleProtocolHandler({
1155
- getRules: () => [...rulebookRules, ...alwaysApplyRules],
1156
+ getRules: () => [...getBundledRules(), ...rulebookRules, ...alwaysApplyRules],
1156
1157
  }),
1157
1158
  );
1158
1159
  internalRouter.register(
@@ -1565,11 +1566,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1565
1566
  } else {
1566
1567
  void svc.getOrRefreshIndex();
1567
1568
  }
1568
- if (cached && cached.products.length > 0) {
1569
- knowledgeTopics = cached.products
1570
- .map(p => p.name)
1571
- .sort()
1572
- .join(", ");
1569
+ if (cached) {
1570
+ knowledgeTopics = svc.getTopicSummary() || undefined;
1573
1571
  }
1574
1572
  }
1575
1573
  } catch {
@@ -2,65 +2,78 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { logger } from "@f5-sales-demo/pi-utils";
4
4
 
5
- export interface LlmsProduct {
5
+ export const KNOWLEDGE_CACHE_SCHEMA_VERSION = 2 as const;
6
+
7
+ export interface LlmsTopic {
6
8
  name: string;
7
9
  description: string;
8
10
  url: string;
11
+ category: string;
9
12
  }
10
13
 
11
14
  export interface LlmsIndex {
15
+ schemaVersion: typeof KNOWLEDGE_CACHE_SCHEMA_VERSION;
12
16
  title: string;
13
17
  description: string;
14
- products: LlmsProduct[];
18
+ topics: LlmsTopic[];
15
19
  fetchedAt: string;
16
20
  }
17
21
 
18
- /**
19
- * Federated sites that are not F5 XC *product* documentation, and so do not belong
20
- * in the product list this module feeds to the system prompt.
21
- *
22
- * Deliberately NOT the `repo_classes` authority classification from
23
- * `xcsh://fleet`. The two answer different questions and disagree: `docs`,
24
- * `cdn-simulator` and `origin-server` are all authority `content` — xcsh does author
25
- * their documentation and Terraform — while being the documentation portal and two
26
- * lab-infrastructure repositories rather than products. Driving this filter off
27
- * authority would advertise all three as F5 XC products.
28
- *
29
- * The concern this tracks is the `llms-federated-sites.json` category in the `docs`
30
- * repository: everything here is `build-platform`, `developer-tools`, `portal` or
31
- * `lab-infrastructure`, and everything kept is `product-features` or
32
- * `api-specifications`. That file is not synced into clones, so the list is
33
- * maintained here.
34
- */
35
- const NON_PRODUCT_SLUGS = new Set([
36
- "docs-builder",
37
- "docs-theme",
38
- "docs-icons",
39
- "devcontainer",
40
- "xcsh",
41
- "docs",
42
- "cdn-simulator",
43
- "origin-server",
44
- ]);
45
-
46
22
  const ENTRY_PATTERN = /^- \[([^\]]+)\]\(([^)]+)\):\s*(.+)$/;
23
+ const ROOT_LLMS_FILE = "llms.txt";
24
+ const PORTAL_SLUG = "docs";
47
25
 
48
- function extractSlug(url: string): string | null {
26
+ function extractRootLlmsSlug(url: string): string | null {
49
27
  try {
50
- const pathname = new URL(url).pathname;
51
- const segments = pathname.split("/").filter(Boolean);
28
+ const segments = new URL(url).pathname.split("/").filter(Boolean);
29
+ if (segments.length !== 2 || segments[1] !== ROOT_LLMS_FILE) return null;
52
30
  return segments[0] ?? null;
53
31
  } catch {
54
32
  return null;
55
33
  }
56
34
  }
57
35
 
36
+ function isTopic(value: unknown): value is LlmsTopic {
37
+ if (!value || typeof value !== "object") return false;
38
+ const topic = value as Partial<LlmsTopic>;
39
+ return (
40
+ typeof topic.name === "string" &&
41
+ typeof topic.description === "string" &&
42
+ typeof topic.url === "string" &&
43
+ typeof topic.category === "string"
44
+ );
45
+ }
46
+
47
+ function isValidIndex(value: unknown): value is LlmsIndex {
48
+ if (!value || typeof value !== "object") return false;
49
+ const index = value as Partial<LlmsIndex>;
50
+ return (
51
+ index.schemaVersion === KNOWLEDGE_CACHE_SCHEMA_VERSION &&
52
+ typeof index.title === "string" &&
53
+ typeof index.description === "string" &&
54
+ typeof index.fetchedAt === "string" &&
55
+ !Number.isNaN(new Date(index.fetchedAt).getTime()) &&
56
+ Array.isArray(index.topics) &&
57
+ index.topics.length > 0 &&
58
+ index.topics.every(isTopic)
59
+ );
60
+ }
61
+
62
+ /**
63
+ * Parse the root llms.txt portal into categorized documentation topics.
64
+ *
65
+ * Federation category headings are intentionally open-ended. A list entry is a
66
+ * 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.
69
+ */
58
70
  export function parseLlmsTxt(content: string, now?: Date): LlmsIndex {
59
71
  const lines = content.split("\n");
60
72
  let title = "";
61
73
  let description = "";
62
- const products: LlmsProduct[] = [];
63
- let inFederatedSites = false;
74
+ let category = "";
75
+ const topics: LlmsTopic[] = [];
76
+ const seenUrls = new Set<string>();
64
77
 
65
78
  for (const line of lines) {
66
79
  const trimmed = line.trim();
@@ -76,26 +89,26 @@ export function parseLlmsTxt(content: string, now?: Date): LlmsIndex {
76
89
  }
77
90
 
78
91
  if (trimmed.startsWith("## ")) {
79
- inFederatedSites = trimmed === "## Federated Sites";
92
+ category = trimmed.slice(3).trim();
80
93
  continue;
81
94
  }
82
95
 
83
- if (!inFederatedSites) continue;
84
-
85
96
  const match = ENTRY_PATTERN.exec(trimmed);
86
- if (!match) continue;
97
+ if (!match || !category) continue;
87
98
 
88
- const [, name, url, desc] = match;
89
- const slug = extractSlug(url);
90
- if (slug && NON_PRODUCT_SLUGS.has(slug)) continue;
99
+ const [, name, url, topicDescription] = match;
100
+ const slug = extractRootLlmsSlug(url);
101
+ if (!slug || slug === PORTAL_SLUG || seenUrls.has(url)) continue;
91
102
 
92
- products.push({ name, description: desc, url });
103
+ seenUrls.add(url);
104
+ topics.push({ name, description: topicDescription, url, category });
93
105
  }
94
106
 
95
107
  return {
108
+ schemaVersion: KNOWLEDGE_CACHE_SCHEMA_VERSION,
96
109
  title,
97
110
  description,
98
- products,
111
+ topics,
99
112
  fetchedAt: (now ?? new Date()).toISOString(),
100
113
  };
101
114
  }
@@ -140,14 +153,15 @@ export class KnowledgeService {
140
153
  loadCache(): void {
141
154
  try {
142
155
  if (!fs.existsSync(this.cachePath)) return;
143
- const raw = fs.readFileSync(this.cachePath, "utf-8");
144
- this.#index = JSON.parse(raw) as LlmsIndex;
156
+ const parsed: unknown = JSON.parse(fs.readFileSync(this.cachePath, "utf-8"));
157
+ this.#index = isValidIndex(parsed) ? parsed : null;
145
158
  } catch {
146
159
  this.#index = null;
147
160
  }
148
161
  }
149
162
 
150
163
  saveCache(index: LlmsIndex): void {
164
+ if (!isValidIndex(index)) return;
151
165
  try {
152
166
  fs.mkdirSync(this.#configDir, { recursive: true });
153
167
  fs.writeFileSync(this.cachePath, JSON.stringify(index, null, 2));
@@ -167,8 +181,10 @@ export class KnowledgeService {
167
181
  if (!response.ok) {
168
182
  throw new Error(`Failed to fetch llms.txt: HTTP ${response.status}`);
169
183
  }
170
- const content = await response.text();
171
- const index = parseLlmsTxt(content);
184
+ const index = parseLlmsTxt(await response.text());
185
+ if (index.topics.length === 0) {
186
+ throw new Error("Failed to parse llms.txt: no federated documentation topics found");
187
+ }
172
188
  this.#index = index;
173
189
  this.saveCache(index);
174
190
  return index;
@@ -187,8 +203,22 @@ export class KnowledgeService {
187
203
  }
188
204
  }
189
205
 
190
- getProductNames(): string[] {
206
+ getTopicNames(): string[] {
191
207
  if (!this.#index) return [];
192
- return this.#index.products.map(p => p.name).sort();
208
+ return this.#index.topics.map(topic => topic.name).sort();
209
+ }
210
+
211
+ getTopicSummary(): string {
212
+ if (!this.#index) return "";
213
+ const topicsByCategory = new Map<string, string[]>();
214
+ for (const topic of this.#index.topics) {
215
+ const names = topicsByCategory.get(topic.category) ?? [];
216
+ names.push(topic.name);
217
+ topicsByCategory.set(topic.category, names);
218
+ }
219
+ return [...topicsByCategory]
220
+ .sort(([left], [right]) => left.localeCompare(right))
221
+ .map(([category, names]) => `${category}: ${names.sort().join(", ")}`)
222
+ .join("; ");
193
223
  }
194
224
  }