@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 +8 -8
- package/src/bundled-rules.ts +35 -0
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/console-catalog.generated.ts +2 -2
- package/src/prompts/rules/epistemic-integrity.md +33 -0
- package/src/prompts/rules/llms-search.md +19 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +16 -9
- package/src/sdk.ts +4 -6
- package/src/services/xcsh-knowledge.ts +80 -50
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.
|
|
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.
|
|
65
|
-
"@f5-sales-demo/pi-agent-core": "20.6.
|
|
66
|
-
"@f5-sales-demo/pi-ai": "20.6.
|
|
67
|
-
"@f5-sales-demo/pi-natives": "20.6.
|
|
68
|
-
"@f5-sales-demo/pi-resource-management": "20.6.
|
|
69
|
-
"@f5-sales-demo/pi-tui": "20.6.
|
|
70
|
-
"@f5-sales-demo/pi-utils": "20.6.
|
|
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.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "20.6.2",
|
|
21
|
+
"commit": "153e66d64c03537aa7e146847f6845b18c1778c2",
|
|
22
|
+
"shortCommit": "153e66d",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v20.6.
|
|
25
|
-
"commitDate": "2026-08-
|
|
26
|
-
"buildDate": "2026-08-
|
|
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/
|
|
32
|
-
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v20.6.
|
|
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 = "
|
|
5
|
+
export const CONSOLE_CATALOG_VERSION = "6e999bfa3fa4d75bfff9cbf55153df7dc063e037";
|
|
6
6
|
|
|
7
7
|
export const CONSOLE_CATALOG_DATA: ConsoleCatalogData = {
|
|
8
|
-
version: "
|
|
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
|
|
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
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
|
458
|
-
llms.txt hierarchy (`https://f5-sales-demo.github.io/docs/llms.txt`). Read
|
|
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
|
|
1569
|
-
knowledgeTopics =
|
|
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
|
|
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
|
-
|
|
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
|
|
26
|
+
function extractRootLlmsSlug(url: string): string | null {
|
|
49
27
|
try {
|
|
50
|
-
const
|
|
51
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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,
|
|
89
|
-
const slug =
|
|
90
|
-
if (slug
|
|
99
|
+
const [, name, url, topicDescription] = match;
|
|
100
|
+
const slug = extractRootLlmsSlug(url);
|
|
101
|
+
if (!slug || slug === PORTAL_SLUG || seenUrls.has(url)) continue;
|
|
91
102
|
|
|
92
|
-
|
|
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
|
-
|
|
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
|
|
144
|
-
this.#index =
|
|
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
|
|
171
|
-
|
|
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
|
-
|
|
206
|
+
getTopicNames(): string[] {
|
|
191
207
|
if (!this.#index) return [];
|
|
192
|
-
return this.#index.
|
|
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
|
}
|