@f5xc-salesdemos/xcsh 18.19.2 → 18.20.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5xc-salesdemos/xcsh",
4
- "version": "18.19.2",
4
+ "version": "18.20.0",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/f5xc-salesdemos/xcsh",
7
7
  "author": "Can Boluk",
@@ -47,12 +47,12 @@
47
47
  "dependencies": {
48
48
  "@agentclientprotocol/sdk": "0.16.1",
49
49
  "@mozilla/readability": "^0.6",
50
- "@f5xc-salesdemos/xcsh-stats": "18.19.2",
51
- "@f5xc-salesdemos/pi-agent-core": "18.19.2",
52
- "@f5xc-salesdemos/pi-ai": "18.19.2",
53
- "@f5xc-salesdemos/pi-natives": "18.19.2",
54
- "@f5xc-salesdemos/pi-tui": "18.19.2",
55
- "@f5xc-salesdemos/pi-utils": "18.19.2",
50
+ "@f5xc-salesdemos/xcsh-stats": "18.20.0",
51
+ "@f5xc-salesdemos/pi-agent-core": "18.20.0",
52
+ "@f5xc-salesdemos/pi-ai": "18.20.0",
53
+ "@f5xc-salesdemos/pi-natives": "18.20.0",
54
+ "@f5xc-salesdemos/pi-tui": "18.20.0",
55
+ "@f5xc-salesdemos/pi-utils": "18.20.0",
56
56
  "@sinclair/typebox": "^0.34",
57
57
  "@xterm/headless": "^6.0",
58
58
  "ajv": "^8.18",
@@ -14,6 +14,7 @@ export interface SkillFrontmatter {
14
14
  description?: string;
15
15
  globs?: string[];
16
16
  alwaysApply?: boolean;
17
+ contexts?: string[];
17
18
  [key: string]: unknown;
18
19
  }
19
20
 
@@ -14,6 +14,7 @@ export interface Skill {
14
14
  filePath: string;
15
15
  baseDir: string;
16
16
  source: string;
17
+ contexts?: string[];
17
18
  /** Source metadata for display */
18
19
  _source?: SourceMeta;
19
20
  }
@@ -56,6 +57,9 @@ export async function loadSkillsFromDir(options: LoadSkillsFromDirOptions): Prom
56
57
  filePath: capSkill.path,
57
58
  baseDir: capSkill.path.replace(/[\\/]SKILL\.md$/, ""),
58
59
  source: options.source,
60
+ contexts: Array.isArray(capSkill.frontmatter?.contexts)
61
+ ? (capSkill.frontmatter.contexts as string[])
62
+ : undefined,
59
63
  _source: capSkill._source,
60
64
  })),
61
65
  warnings: (result.warnings ?? []).map(message => ({ skillPath: options.dir, message })),
@@ -170,6 +174,9 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
170
174
  filePath: capSkill.path,
171
175
  baseDir: capSkill.path.replace(/[\\/]SKILL\.md$/, ""),
172
176
  source: `${capSkill._source.provider}:${capSkill.level}`,
177
+ contexts: Array.isArray(capSkill.frontmatter?.contexts)
178
+ ? (capSkill.frontmatter.contexts as string[])
179
+ : undefined,
173
180
  _source: capSkill._source,
174
181
  });
175
182
  realPathSet.add(resolvedPath);
@@ -206,6 +213,9 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
206
213
  filePath: capSkill.path,
207
214
  baseDir: capSkill.path.replace(/[\\/]SKILL\.md$/, ""),
208
215
  source: "custom:user",
216
+ contexts: Array.isArray(capSkill.frontmatter?.contexts)
217
+ ? (capSkill.frontmatter.contexts as string[])
218
+ : undefined,
209
219
  _source: { ...capSkill._source, providerName: "Custom" },
210
220
  },
211
221
  path: capSkill.path,
@@ -250,3 +260,9 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
250
260
  warnings: [...(result.warnings ?? []).map(w => ({ skillPath: "", message: w })), ...collisionWarnings],
251
261
  };
252
262
  }
263
+
264
+ export function isApplicableToContext(skill: Skill, contextName?: string): boolean {
265
+ if (!skill.contexts || skill.contexts.length === 0) return true;
266
+ if (!contextName) return true;
267
+ return skill.contexts.includes(contextName);
268
+ }
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "18.19.2",
21
- "commit": "aadf3b16536d2269c1e1ff2ee1aefa406c1f4ecc",
22
- "shortCommit": "aadf3b1",
20
+ "version": "18.20.0",
21
+ "commit": "6feb4da2091180b8eb6488f3991608b55ce3e6a8",
22
+ "shortCommit": "6feb4da",
23
23
  "branch": "main",
24
- "tag": "v18.19.2",
25
- "commitDate": "2026-04-27T21:44:40Z",
26
- "buildDate": "2026-04-27T22:06:55.819Z",
24
+ "tag": "v18.20.0",
25
+ "commitDate": "2026-04-28T04:58:55Z",
26
+ "buildDate": "2026-04-28T05:21:14.340Z",
27
27
  "dirty": false,
28
28
  "prNumber": "",
29
29
  "repoUrl": "https://github.com/f5xc-salesdemos/xcsh",
30
30
  "repoSlug": "f5xc-salesdemos/xcsh",
31
- "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/aadf3b16536d2269c1e1ff2ee1aefa406c1f4ecc",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.19.2"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/6feb4da2091180b8eb6488f3991608b55ce3e6a8",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.20.0"
33
33
  };
@@ -39,6 +39,9 @@ Credential source: {{context.credentialSource}}.
39
39
  Auth status: {{context.authStatus}}.
40
40
  All F5 XC operations should target this tenant and namespace unless explicitly told otherwise.
41
41
  {{/if}}
42
+ {{#if knowledgeTopics}}
43
+ Available F5 XC documentation topics: {{knowledgeTopics}}.
44
+ {{/if}}
42
45
  {{#if skills.length}}
43
46
  Skills are specialized knowledge.
44
47
  You **MUST** scan descriptions for your task domain.
@@ -143,6 +143,10 @@ Auth status: {{context.authStatus}}.
143
143
  All F5 XC operations should target this tenant and namespace unless explicitly told otherwise.
144
144
  {{/if}}
145
145
 
146
+ {{#if knowledgeTopics}}
147
+ Available F5 XC documentation topics: {{knowledgeTopics}}.
148
+ {{/if}}
149
+
146
150
  {{#if contextFiles.length}}
147
151
  <context>
148
152
  Context files below **MUST** be followed for all tasks:
package/src/sdk.ts CHANGED
@@ -738,6 +738,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
738
738
  // Capture ContextService reference for sync consumers (e.g., InternalDocsProtocolHandler's
739
739
  // getContextStatus getter below). Null when ContextService isn't available (SDK consumers, tests).
740
740
  let contextServiceRef: typeof import("./services/f5xc-context").ContextService | null = null;
741
+ let knowledgeServiceRef: typeof import("./services/f5xc-knowledge").KnowledgeService | null = null;
741
742
 
742
743
  // Capture ContextService reference for sync consumers (rebuildSystemPrompt context resolution,
743
744
  // InternalDocsProtocolHandler getContextStatus). The context-change listener itself is
@@ -749,6 +750,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
749
750
  } catch {
750
751
  // ContextService not available (SDK consumers, tests). Skip.
751
752
  }
753
+ try {
754
+ const { KnowledgeService } = await import("./services/f5xc-knowledge");
755
+ const { getF5XCConfigDir } = await import("@f5xc-salesdemos/pi-utils");
756
+ knowledgeServiceRef = KnowledgeService;
757
+ if (!KnowledgeService._hasInstance()) {
758
+ KnowledgeService.init(getF5XCConfigDir());
759
+ KnowledgeService.instance.loadCache();
760
+ }
761
+ } catch {
762
+ // KnowledgeService not available — skip.
763
+ }
752
764
 
753
765
  // Check if session has existing data to restore
754
766
  const existingSession = logger.time("loadSessionContext", () =>
@@ -1370,6 +1382,24 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1370
1382
  // ContextService not available or not initialized — leave contextForPrompt undefined.
1371
1383
  }
1372
1384
 
1385
+ let knowledgeTopics: string | undefined;
1386
+ try {
1387
+ if (knowledgeServiceRef) {
1388
+ const svc = knowledgeServiceRef.instance;
1389
+ const cached = svc.getIndex();
1390
+ if (cached && cached.products.length > 0) {
1391
+ knowledgeTopics = cached.products
1392
+ .map(p => p.name)
1393
+ .sort()
1394
+ .join(", ");
1395
+ }
1396
+ // Fire-and-forget background refresh when TTL is expired — never blocks the prompt.
1397
+ void svc.getOrRefreshIndex();
1398
+ }
1399
+ } catch {
1400
+ // KnowledgeService not available — leave undefined.
1401
+ }
1402
+
1373
1403
  // Build combined append prompt: memory instructions + MCP server instructions
1374
1404
  const serverInstructions = mcpManager?.getServerInstructions();
1375
1405
  let appendPrompt: string | undefined = memoryInstructions ?? undefined;
@@ -1406,6 +1436,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1406
1436
  eagerTasks,
1407
1437
  secretsEnabled,
1408
1438
  context: contextForPrompt,
1439
+ knowledgeTopics,
1409
1440
  });
1410
1441
 
1411
1442
  if (options.systemPrompt === undefined) {
@@ -1430,6 +1461,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1430
1461
  eagerTasks,
1431
1462
  secretsEnabled,
1432
1463
  context: contextForPrompt,
1464
+ knowledgeTopics,
1433
1465
  });
1434
1466
  }
1435
1467
  return options.systemPrompt(defaultPrompt);
@@ -0,0 +1,177 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { logger } from "@f5xc-salesdemos/pi-utils";
4
+
5
+ export interface LlmsProduct {
6
+ name: string;
7
+ description: string;
8
+ url: string;
9
+ }
10
+
11
+ export interface LlmsIndex {
12
+ title: string;
13
+ description: string;
14
+ products: LlmsProduct[];
15
+ fetchedAt: string;
16
+ }
17
+
18
+ const INFRASTRUCTURE_SLUGS = new Set([
19
+ "docs-builder",
20
+ "docs-theme",
21
+ "docs-icons",
22
+ "devcontainer",
23
+ "xcsh",
24
+ "docs",
25
+ "cdn-simulator",
26
+ "origin-server",
27
+ ]);
28
+
29
+ const ENTRY_PATTERN = /^- \[([^\]]+)\]\(([^)]+)\):\s*(.+)$/;
30
+
31
+ function extractSlug(url: string): string | null {
32
+ try {
33
+ const pathname = new URL(url).pathname;
34
+ const segments = pathname.split("/").filter(Boolean);
35
+ return segments[0] ?? null;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ export function parseLlmsTxt(content: string, now?: Date): LlmsIndex {
42
+ const lines = content.split("\n");
43
+ let title = "";
44
+ let description = "";
45
+ const products: LlmsProduct[] = [];
46
+ let inFederatedSites = false;
47
+
48
+ for (const line of lines) {
49
+ const trimmed = line.trim();
50
+
51
+ if (!title && trimmed.startsWith("# ")) {
52
+ title = trimmed.slice(2).trim();
53
+ continue;
54
+ }
55
+
56
+ if (!description && trimmed.startsWith("> ")) {
57
+ description = trimmed.slice(2).trim();
58
+ continue;
59
+ }
60
+
61
+ if (trimmed.startsWith("## ")) {
62
+ inFederatedSites = trimmed === "## Federated Sites";
63
+ continue;
64
+ }
65
+
66
+ if (!inFederatedSites) continue;
67
+
68
+ const match = ENTRY_PATTERN.exec(trimmed);
69
+ if (!match) continue;
70
+
71
+ const [, name, url, desc] = match;
72
+ const slug = extractSlug(url);
73
+ if (slug && INFRASTRUCTURE_SLUGS.has(slug)) continue;
74
+
75
+ products.push({ name, description: desc, url });
76
+ }
77
+
78
+ return {
79
+ title,
80
+ description,
81
+ products,
82
+ fetchedAt: (now ?? new Date()).toISOString(),
83
+ };
84
+ }
85
+
86
+ const ROOT_LLMS_URL = "https://f5xc-salesdemos.github.io/docs/llms.txt";
87
+ const DEFAULT_TTL_MS = 3_600_000;
88
+
89
+ export class KnowledgeService {
90
+ static #instance: KnowledgeService | null = null;
91
+
92
+ #configDir: string;
93
+ #index: LlmsIndex | null = null;
94
+
95
+ private constructor(configDir: string) {
96
+ this.#configDir = configDir;
97
+ }
98
+
99
+ static init(configDir: string): KnowledgeService {
100
+ KnowledgeService.#instance = new KnowledgeService(configDir);
101
+ return KnowledgeService.#instance;
102
+ }
103
+
104
+ static get instance(): KnowledgeService {
105
+ if (!KnowledgeService.#instance) {
106
+ throw new Error("KnowledgeService not initialized. Call KnowledgeService.init() first.");
107
+ }
108
+ return KnowledgeService.#instance;
109
+ }
110
+
111
+ static _resetForTest(): void {
112
+ KnowledgeService.#instance = null;
113
+ }
114
+
115
+ static _hasInstance(): boolean {
116
+ return KnowledgeService.#instance !== null;
117
+ }
118
+
119
+ get cachePath(): string {
120
+ return path.join(this.#configDir, "knowledge-cache.json");
121
+ }
122
+
123
+ loadCache(): void {
124
+ try {
125
+ if (!fs.existsSync(this.cachePath)) return;
126
+ const raw = fs.readFileSync(this.cachePath, "utf-8");
127
+ this.#index = JSON.parse(raw) as LlmsIndex;
128
+ } catch {
129
+ this.#index = null;
130
+ }
131
+ }
132
+
133
+ saveCache(index: LlmsIndex): void {
134
+ try {
135
+ fs.mkdirSync(this.#configDir, { recursive: true });
136
+ fs.writeFileSync(this.cachePath, JSON.stringify(index, null, 2));
137
+ } catch (err) {
138
+ logger.debug("F5XC knowledge cache write failed", { error: String(err) });
139
+ }
140
+ }
141
+
142
+ getIndex(): LlmsIndex | null {
143
+ return this.#index;
144
+ }
145
+
146
+ async refreshIndex(): Promise<LlmsIndex> {
147
+ const response = await fetch(ROOT_LLMS_URL, {
148
+ signal: AbortSignal.timeout(10_000),
149
+ });
150
+ if (!response.ok) {
151
+ throw new Error(`Failed to fetch llms.txt: HTTP ${response.status}`);
152
+ }
153
+ const content = await response.text();
154
+ const index = parseLlmsTxt(content);
155
+ this.#index = index;
156
+ this.saveCache(index);
157
+ return index;
158
+ }
159
+
160
+ async getOrRefreshIndex(ttlMs = DEFAULT_TTL_MS): Promise<LlmsIndex | null> {
161
+ if (this.#index) {
162
+ const age = Date.now() - new Date(this.#index.fetchedAt).getTime();
163
+ if (age < ttlMs) return this.#index;
164
+ }
165
+ try {
166
+ return await this.refreshIndex();
167
+ } catch (err) {
168
+ logger.debug("F5XC knowledge index refresh failed, using stale cache", { error: String(err) });
169
+ return this.#index;
170
+ }
171
+ }
172
+
173
+ getProductNames(): string[] {
174
+ if (!this.#index) return [];
175
+ return this.#index.products.map(p => p.name).sort();
176
+ }
177
+ }
@@ -12,7 +12,7 @@ import { contextFileCapability } from "./capability/context-file";
12
12
  import { systemPromptCapability } from "./capability/system-prompt";
13
13
  import type { SkillsSettings } from "./config/settings";
14
14
  import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile } from "./discovery";
15
- import { loadSkills, type Skill } from "./extensibility/skills";
15
+ import { isApplicableToContext, loadSkills, type Skill } from "./extensibility/skills";
16
16
  import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
17
17
  import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
18
18
 
@@ -441,6 +441,7 @@ export interface BuildSystemPromptOptions {
441
441
  credentialSource: string;
442
442
  authStatus: string;
443
443
  };
444
+ knowledgeTopics?: string;
444
445
  }
445
446
 
446
447
  /** Build the system prompt with tools, guidelines, and context */
@@ -584,6 +585,11 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
584
585
  const hasRead = tools?.has("read");
585
586
  const filteredSkills = hasRead ? skills : [];
586
587
 
588
+ // contexts values match the tenant label derived from the API URL hostname (first DNS label).
589
+ // Example: https://acme.console.ves.volterra.io → tenant "acme" → contexts: ["acme"]
590
+ const contextName = context?.tenant;
591
+ const contextFilteredSkills = filteredSkills.filter(s => isApplicableToContext(s, contextName));
592
+
587
593
  const effectiveSystemPromptCustomization = dedupePromptSource(systemPromptCustomization, [
588
594
  resolvedCustomPrompt,
589
595
  resolvedAppendPrompt,
@@ -602,7 +608,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
602
608
  environment,
603
609
  contextFiles,
604
610
  agentsMdSearch,
605
- skills: filteredSkills,
611
+ skills: contextFilteredSkills,
606
612
  rules: rules ?? [],
607
613
  alwaysApplyRules: injectedAlwaysApplyRules,
608
614
  date,
@@ -616,6 +622,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
616
622
  eagerTasks,
617
623
  secretsEnabled,
618
624
  context,
625
+ knowledgeTopics: options.knowledgeTopics,
619
626
  };
620
627
  let rendered = prompt.render(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
621
628