@f5xc-salesdemos/xcsh 18.72.0 → 18.77.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.75.0] - 2026-05-23
6
+
7
+ ### Added
8
+
9
+ - Auto-validate credentials on context switch: SDK context change listener now fires a background `validateToken()` after emitting the switch notification, sending a follow-up `context_validation_result` custom message so the LLM knows whether the new context's credentials are valid
10
+
5
11
  ## [18.64.2] - 2026-05-16
6
12
 
7
13
  ### Fixed
@@ -20,11 +26,13 @@
20
26
  ### Changed
21
27
 
22
28
  - Regenerated API spec index from catalog v2.1.82: http_loadbalancer CRUD verification corrections (6 new server defaults, corrected minimum configs, cross-field dependencies, default_pool inline pool discovery, 5 composable routing approaches) and tcp_loadbalancer minimum config corrections (listen_port, origin_pools_weights, do_not_advertise format, 9 server defaults, forced hash_policy default) ([#753](https://github.com/f5xc-salesdemos/xcsh/issues/753), [#757](https://github.com/f5xc-salesdemos/xcsh/issues/757))
29
+
23
30
  ## [18.53.0] - 2026-05-09
24
31
 
25
32
  ### Changed
26
33
 
27
34
  - Autoresearch subsystem code quality: -513 lines (18.8% reduction), ~13% faster type checking. Un-exported internal symbols, relocated types, consolidated duplicate patterns, replaced manual deep copies with `structuredClone`, replaced `while(exec)` with `matchAll`, compressed control flow, extracted shared interfaces ([#734](https://github.com/f5xc-salesdemos/xcsh/pull/734))
35
+
28
36
  ## [18.53.0] - 2026-05-09
29
37
 
30
38
  ### Fixed
@@ -79,13 +87,11 @@
79
87
 
80
88
  ## [18.18.4] - 2026-04-26
81
89
 
82
-
83
90
  ### Fixed
84
91
 
85
92
  - Fixed gutter width propagation in the fallback tool renderer: `#formatToolExecution()` now receives the actual available width at render-time and uses it for line truncation instead of a hardcoded 80-column limit. On narrow terminals (<82 cols) this prevents content wider than the gutter-adjusted viewport; on wide terminals it allows longer output lines. ([#117](https://github.com/f5xc-salesdemos/xcsh/issues/117))
86
93
  - Fixed `resolveConfigValue` returning literal env var names (e.g. `"LITELLM_API_KEY"`) as API keys when the env var is unset, causing 401 errors on first launch. The resolver now rejects unresolved `ALL_CAPS_WITH_UNDERSCORES` patterns, matching the existing guard in `resolveYamlApiKeyConfig`. ([#241](https://github.com/f5xc-salesdemos/xcsh/issues/241))
87
94
 
88
-
89
95
  ### Changed
90
96
 
91
97
  - Renamed F5 XC credential system from "profile" to "context" to align with kubectl conventions. The `/profile` command is now `/context`, all types/classes use `Context*` naming (`ContextService`, `ContextStatus`, `F5XCContext`, etc.), on-disk paths changed from `profiles/` to `contexts/` and `active_profile` to `active_context`, and the status-line segment ID is now `context_f5xc`. ([#302](https://github.com/f5xc-salesdemos/xcsh/issues/302))
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5xc-salesdemos/xcsh",
4
- "version": "18.72.0",
4
+ "version": "18.77.1",
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",
@@ -48,12 +48,12 @@
48
48
  "dependencies": {
49
49
  "@agentclientprotocol/sdk": "0.16.1",
50
50
  "@mozilla/readability": "^0.6",
51
- "@f5xc-salesdemos/xcsh-stats": "18.72.0",
52
- "@f5xc-salesdemos/pi-agent-core": "18.72.0",
53
- "@f5xc-salesdemos/pi-ai": "18.72.0",
54
- "@f5xc-salesdemos/pi-natives": "18.72.0",
55
- "@f5xc-salesdemos/pi-tui": "18.72.0",
56
- "@f5xc-salesdemos/pi-utils": "18.72.0",
51
+ "@f5xc-salesdemos/xcsh-stats": "18.77.1",
52
+ "@f5xc-salesdemos/pi-agent-core": "18.77.1",
53
+ "@f5xc-salesdemos/pi-ai": "18.77.1",
54
+ "@f5xc-salesdemos/pi-natives": "18.77.1",
55
+ "@f5xc-salesdemos/pi-tui": "18.77.1",
56
+ "@f5xc-salesdemos/pi-utils": "18.77.1",
57
57
  "@sinclair/typebox": "^0.34",
58
58
  "@xterm/headless": "^6.0",
59
59
  "ajv": "^8.18",
@@ -10,6 +10,12 @@ interface SpecPathOperation {
10
10
  [key: string]: unknown;
11
11
  }
12
12
 
13
+ const DEDUP_SUFFIX_RE = /_(get|post|put|delete|patch)(_\d+)?$/;
14
+
15
+ function normalizeOperationId(opId: string): string {
16
+ return opId.replace(DEDUP_SUFFIX_RE, "");
17
+ }
18
+
13
19
  function findResourceSchemaComponents(
14
20
  resourceName: string,
15
21
  paths: Record<string, Record<string, SpecPathOperation>>,
@@ -25,7 +31,7 @@ function findResourceSchemaComponents(
25
31
  for (const op of Object.values(methods)) {
26
32
  const opId = op?.operationId;
27
33
  if (!opId) continue;
28
- const match = opId.match(/^ves\.io\.schema\.(.+?)\.(?:API|CustomAPI)\./);
34
+ const match = normalizeOperationId(opId).match(/^ves\.io\.schema\.(.+?)\.(?:API|CustomAPI)\./);
29
35
  if (match) found.add(match[1]);
30
36
  }
31
37
  }
@@ -214,6 +220,29 @@ async function downloadCatalog(specsDir: string): Promise<Record<string, unknown
214
220
  }
215
221
  }
216
222
 
223
+ async function downloadValidation(specsDir: string): Promise<Record<string, unknown> | null> {
224
+ const validationPath = path.join(specsDir, "validation.json");
225
+ if (fs.existsSync(validationPath)) {
226
+ return JSON.parse(fs.readFileSync(validationPath, "utf-8"));
227
+ }
228
+
229
+ const validationTag = process.env.API_SPECS_TAG ?? (await resolveLatestTag());
230
+ const validationUrl = `https://github.com/${REPO}/releases/download/${validationTag}/validation.json`;
231
+ console.log(`Downloading validation.json from ${validationUrl}...`);
232
+ try {
233
+ const response = await fetchWithRetry(validationUrl, { redirect: "follow" });
234
+ if (!response.ok) {
235
+ console.warn(`validation.json not found (${response.status}), skipping validation data generation`);
236
+ return null;
237
+ }
238
+ const text = await response.text();
239
+ return JSON.parse(text);
240
+ } catch (err) {
241
+ console.warn(`Failed to download validation.json: ${err instanceof Error ? err.message : err}`);
242
+ return null;
243
+ }
244
+ }
245
+
217
246
  function serializeEnrichment(key: string, value: unknown): string | undefined {
218
247
  if (!value) return undefined;
219
248
  return `\t${key}: ${JSON.stringify(value)},`;
@@ -238,6 +267,7 @@ if (!fs.existsSync(indexPath)) {
238
267
  const rawIndex: RawIndex = JSON.parse(fs.readFileSync(indexPath, "utf-8"));
239
268
 
240
269
  const catalog = await downloadCatalog(specsDir);
270
+ const validation = await downloadValidation(specsDir);
241
271
 
242
272
  const pathToCatalogCategories = new Map<string, string[]>();
243
273
  if (catalog) {
@@ -441,8 +471,12 @@ for (const entry of rawIndex.specifications) {
441
471
  const schemaEnrichments: Record<string, Record<string, unknown>> = {};
442
472
  for (const [schemaName, schemaDef] of Object.entries(enrichSpecJson.components?.schemas ?? {})) {
443
473
  const rec = schemaDef["x-f5xc-recommended-oneof-variant"] as Record<string, string> | undefined;
444
- if (rec) {
445
- schemaEnrichments[schemaName] = { recommendedOneofVariant: rec };
474
+ const minConfig = schemaDef["x-f5xc-minimum-configuration"] as Record<string, unknown> | undefined;
475
+ if (rec || minConfig) {
476
+ schemaEnrichments[schemaName] = {
477
+ ...(rec ? { recommendedOneofVariant: rec } : {}),
478
+ ...(minConfig ? { minimumConfiguration: minConfig } : {}),
479
+ };
446
480
  }
447
481
  }
448
482
 
@@ -456,7 +490,7 @@ for (const entry of rawIndex.specifications) {
456
490
  const output = [
457
491
  "// Auto-generated by scripts/generate-api-spec-index.ts - DO NOT EDIT",
458
492
  "",
459
- `import type { ApiSpecDomainEnrichments, ApiSpecIndex } from "./api-spec-types";`,
493
+ `import type { ApiSpecDomainEnrichments, ApiSpecIndex, ApiSpecValidationResourceEntry } from "./api-spec-types";`,
460
494
  "",
461
495
  `export const API_SPEC_VERSION = ${JSON.stringify(rawIndex.version)};`,
462
496
  "",
@@ -480,6 +514,12 @@ const output = [
480
514
  ...enrichmentEntries,
481
515
  `};`,
482
516
  "",
517
+ ...(validation
518
+ ? [
519
+ `export const API_VALIDATION_DATA: Readonly<Record<string, ApiSpecValidationResourceEntry>> = ${JSON.stringify((validation as { required_fields?: { resources?: Record<string, unknown> } }).required_fields?.resources ?? {})};`,
520
+ "",
521
+ ]
522
+ : [`export const API_VALIDATION_DATA: Readonly<Record<string, ApiSpecValidationResourceEntry>> = {};`, ""]),
483
523
  ]
484
524
  .filter(l => l !== undefined)
485
525
  .join("\n");
@@ -1199,6 +1199,16 @@ export const SETTINGS_SCHEMA = {
1199
1199
  },
1200
1200
  },
1201
1201
 
1202
+ "todo.verbose": {
1203
+ type: "boolean",
1204
+ default: false,
1205
+ ui: {
1206
+ tab: "tools",
1207
+ label: "Show Todo Tool Output",
1208
+ description: "Display todo_write tool calls and reminders in the chat transcript",
1209
+ },
1210
+ },
1211
+
1202
1212
  // Search and AST tools
1203
1213
  "find.enabled": {
1204
1214
  type: "boolean",
@@ -2,6 +2,8 @@ import type {
2
2
  ApiSpecDomainEnrichments,
3
3
  ApiSpecDomainEntry,
4
4
  ApiSpecIndex,
5
+ ApiSpecMinimumConfiguration,
6
+ ApiSpecValidationResourceEntry,
5
7
  OpenAPIPathOperation,
6
8
  OpenAPISpec,
7
9
  } from "./api-spec-types";
@@ -10,6 +12,12 @@ import type { InternalResource, InternalUrl } from "./types";
10
12
  const SCHEMA_RENDER_MAX_DEPTH = 3;
11
13
  const CRUD_OPERATION_SUFFIXES = [".API.Create", ".API.Replace", ".API.Get", ".API.List", ".API.Delete"];
12
14
 
15
+ const DEDUP_SUFFIX_RE = /_(get|post|put|delete|patch)(_\d+)?$/;
16
+
17
+ function normalizeOperationId(opId: string): string {
18
+ return opId.replace(DEDUP_SUFFIX_RE, "");
19
+ }
20
+
13
21
  const groupsCache = new WeakMap<OpenAPISpec, Map<string, Record<string, Record<string, OpenAPIPathOperation>>>>();
14
22
 
15
23
  function getCachedGroups(spec: OpenAPISpec): Map<string, Record<string, Record<string, OpenAPIPathOperation>>> {
@@ -28,6 +36,7 @@ export function createApiSpecResolver(
28
36
  index: ApiSpecIndex,
29
37
  data: Readonly<Record<string, OpenAPISpec>>,
30
38
  enrichments?: Readonly<Record<string, ApiSpecDomainEnrichments>>,
39
+ validationData?: Readonly<Record<string, ApiSpecValidationResourceEntry>>,
31
40
  ): ApiSpecResolver {
32
41
  function lookup(domain: string): OpenAPISpec {
33
42
  const spec = data[domain];
@@ -59,6 +68,16 @@ export function createApiSpecResolver(
59
68
  return makeResource(url, renderGlossary(index));
60
69
  }
61
70
 
71
+ if (domain === "validation" || domain.startsWith("validation/")) {
72
+ const resourceKey = domain.replace(/^validation\/?/, "");
73
+ return makeResource(
74
+ url,
75
+ resourceKey
76
+ ? renderValidationDetail(resourceKey, validationData)
77
+ : renderValidationIndex(validationData),
78
+ );
79
+ }
80
+
62
81
  const entry = index.domains.find(d => d.domain === domain);
63
82
  if (!entry) {
64
83
  return makeResource(url, renderUnknownDomain(domain, index));
@@ -77,7 +96,15 @@ export function createApiSpecResolver(
77
96
  }
78
97
  return makeResource(
79
98
  url,
80
- renderResourceSpec(domain, resource, spec, entry, { crudOnly: crud }, enrichments?.[domain]),
99
+ renderResourceSpec(
100
+ domain,
101
+ resource,
102
+ spec,
103
+ entry,
104
+ { crudOnly: crud },
105
+ enrichments?.[domain],
106
+ validationData,
107
+ ),
81
108
  );
82
109
  }
83
110
 
@@ -244,7 +271,7 @@ function renderDomainDetail(domain: string, entry: ApiSpecDomainEntry, spec: Ope
244
271
  }
245
272
 
246
273
  function extractSchemaComponent(operationId: string): string | null {
247
- const match = operationId.match(/^ves\.io\.schema\.(.+?)\.(?:API|CustomAPI)\./);
274
+ const match = normalizeOperationId(operationId).match(/^ves\.io\.schema\.(.+?)\.(?:API|CustomAPI)\./);
248
275
  return match ? match[1] : null;
249
276
  }
250
277
 
@@ -348,6 +375,78 @@ function filterPathsByResource(
348
375
  return result;
349
376
  }
350
377
 
378
+ function lookupMinConfig(
379
+ schemaKey: string,
380
+ enrichments: ApiSpecDomainEnrichments,
381
+ ): ApiSpecMinimumConfiguration | undefined {
382
+ const direct = enrichments.schemaEnrichments[schemaKey]?.minimumConfiguration;
383
+ if (direct) return direct as ApiSpecMinimumConfiguration;
384
+ const createReq = enrichments.schemaEnrichments[`${schemaKey}CreateRequest`]?.minimumConfiguration;
385
+ if (createReq) return createReq as ApiSpecMinimumConfiguration;
386
+ const leaf = schemaKey.includes(".") ? schemaKey.split(".").at(-1) : undefined;
387
+ if (leaf) {
388
+ const leafDirect = enrichments.schemaEnrichments[leaf]?.minimumConfiguration;
389
+ if (leafDirect) return leafDirect as ApiSpecMinimumConfiguration;
390
+ const leafCreateReq = enrichments.schemaEnrichments[`${leaf}CreateRequest`]?.minimumConfiguration;
391
+ if (leafCreateReq) return leafCreateReq as ApiSpecMinimumConfiguration;
392
+ }
393
+ return undefined;
394
+ }
395
+
396
+ function findResourceMinConfig(
397
+ resource: string,
398
+ entry: ApiSpecDomainEntry | undefined,
399
+ matchingPaths: Record<string, Record<string, OpenAPIPathOperation>>,
400
+ domainEnrichments?: ApiSpecDomainEnrichments,
401
+ ): ApiSpecMinimumConfiguration | undefined {
402
+ if (!domainEnrichments) return undefined;
403
+
404
+ if (entry) {
405
+ const indexedResource = entry.resources.find(r => r.name === resource);
406
+ if (indexedResource?.schemaComponents?.length) {
407
+ const mc = lookupMinConfig(indexedResource.schemaComponents[0], domainEnrichments);
408
+ if (mc) return mc;
409
+ }
410
+ }
411
+
412
+ for (const methods of Object.values(matchingPaths)) {
413
+ for (const op of Object.values(methods)) {
414
+ if (typeof op !== "object" || !op) continue;
415
+ const opId = op.operationId;
416
+ if (!opId) continue;
417
+ const schema = extractSchemaComponent(opId);
418
+ if (schema) {
419
+ const mc = lookupMinConfig(schema, domainEnrichments);
420
+ if (mc) return mc;
421
+ }
422
+ }
423
+ }
424
+
425
+ return undefined;
426
+ }
427
+
428
+ function renderMinimumConfigSection(mc: ApiSpecMinimumConfiguration): string {
429
+ const sections: string[] = ["## Quick Start — Minimum Configuration", ""];
430
+
431
+ if (mc.required_fields.length > 0) {
432
+ sections.push(`**Required fields:** ${mc.required_fields.join(", ")}`, "");
433
+ }
434
+
435
+ if (mc.example_json) {
436
+ sections.push("### JSON Example", "", "```json", mc.example_json, "```", "");
437
+ }
438
+
439
+ if (mc.example_yaml) {
440
+ sections.push("### YAML Example", "", "```yaml", mc.example_yaml, "```", "");
441
+ }
442
+
443
+ if (mc.example_curl) {
444
+ sections.push("### curl Example", "", "```bash", mc.example_curl, "```", "");
445
+ }
446
+
447
+ return sections.join("\n");
448
+ }
449
+
351
450
  function renderResourceSpec(
352
451
  _domain: string,
353
452
  resource: string,
@@ -355,6 +454,7 @@ function renderResourceSpec(
355
454
  entry?: ApiSpecDomainEntry,
356
455
  options?: { crudOnly?: boolean },
357
456
  domainEnrichments?: ApiSpecDomainEnrichments,
457
+ validationData?: Readonly<Record<string, ApiSpecValidationResourceEntry>>,
358
458
  ): string {
359
459
  const matchingPaths = filterPathsByResource(spec, resource, entry);
360
460
  const label = options?.crudOnly ? "CRUD Operations" : "Full API Specification";
@@ -365,13 +465,16 @@ function renderResourceSpec(
365
465
  if (typeof op !== "object" || !op) continue;
366
466
  if (options?.crudOnly) {
367
467
  const opId = op.operationId ?? "";
368
- if (!CRUD_OPERATION_SUFFIXES.some(s => opId.endsWith(s))) continue;
468
+ if (!CRUD_OPERATION_SUFFIXES.some(s => normalizeOperationId(opId).endsWith(s))) continue;
369
469
  }
370
470
  const operation = op;
371
471
  sections.push(`## ${method.toUpperCase()} ${pathKey}`, "");
372
472
  if (operation.summary) sections.push(String(operation.summary), "");
373
473
 
374
- const opEnrichment = domainEnrichments?.operationMeta[operation.operationId ?? ""];
474
+ const rawOpId = operation.operationId ?? "";
475
+ const opEnrichment =
476
+ domainEnrichments?.operationMeta[rawOpId] ??
477
+ domainEnrichments?.operationMeta[normalizeOperationId(rawOpId)];
375
478
  if (opEnrichment) {
376
479
  const badges: string[] = [];
377
480
  if (opEnrichment.dangerLevel) badges.push(`Danger: **${opEnrichment.dangerLevel}**`);
@@ -468,6 +571,25 @@ function renderResourceSpec(
468
571
  }
469
572
  }
470
573
 
574
+ const minConfig = findResourceMinConfig(resource, entry, matchingPaths, domainEnrichments);
575
+ if (minConfig) {
576
+ sections.push(renderMinimumConfigSection(minConfig));
577
+ }
578
+
579
+ const validationEntry = validationData?.[resource];
580
+ if (validationEntry) {
581
+ sections.push("## Field Requirements", "");
582
+ if (validationEntry.create?.length) {
583
+ sections.push(`**Create:** ${validationEntry.create.join(", ")}`, "");
584
+ }
585
+ if (validationEntry.update?.length) {
586
+ sections.push(`**Update:** ${validationEntry.update.join(", ")}`, "");
587
+ }
588
+ if (validationEntry.minimum_config?.length) {
589
+ sections.push(`**Minimum config:** ${validationEntry.minimum_config.join(", ")}`, "");
590
+ }
591
+ }
592
+
471
593
  return sections.join("\n");
472
594
  }
473
595
 
@@ -817,6 +939,71 @@ function renderGlossary(index: ApiSpecIndex): string {
817
939
  return sections.join("\n");
818
940
  }
819
941
 
942
+ function renderValidationIndex(validationData?: Readonly<Record<string, ApiSpecValidationResourceEntry>>): string {
943
+ if (!validationData || Object.keys(validationData).length === 0) {
944
+ return "# API Field Requirements\n\nNo validation data available.\n";
945
+ }
946
+
947
+ const rows = Object.entries(validationData)
948
+ .sort(([a], [b]) => a.localeCompare(b))
949
+ .map(([name, entry]) => {
950
+ const createFields = entry.create?.join(", ") ?? "—";
951
+ const updateFields = entry.update?.join(", ") ?? "—";
952
+ const minFields = entry.minimum_config?.join(", ") ?? "—";
953
+ return `| ${name} | ${createFields} | ${updateFields} | ${minFields} |`;
954
+ });
955
+
956
+ return [
957
+ "# API Field Requirements",
958
+ "",
959
+ `${Object.keys(validationData).length} resources. Read \`xcsh://api-spec/validation/{resource}\` for details.`,
960
+ "",
961
+ "| Resource | Create Fields | Update Fields | Minimum Config Fields |",
962
+ "|----------|--------------|---------------|----------------------|",
963
+ ...rows,
964
+ "",
965
+ ].join("\n");
966
+ }
967
+
968
+ function renderValidationDetail(
969
+ resource: string,
970
+ validationData?: Readonly<Record<string, ApiSpecValidationResourceEntry>>,
971
+ ): string {
972
+ if (!validationData || Object.keys(validationData).length === 0) {
973
+ return `# ${resource} — Field Requirements\n\nNo validation data available.\n`;
974
+ }
975
+
976
+ const entry = validationData[resource];
977
+ if (!entry) {
978
+ const available = Object.keys(validationData).sort().slice(0, 20);
979
+ return [
980
+ `# Resource not found: ${resource}`,
981
+ "",
982
+ "Available resources:",
983
+ ...available.map(r => `- \`${r}\``),
984
+ "",
985
+ "Use `xcsh://api-spec/validation/{resource}` with one of the above.",
986
+ "",
987
+ ].join("\n");
988
+ }
989
+
990
+ const sections = [`# ${resource} — Field Requirements`, ""];
991
+
992
+ if (entry.create?.length) {
993
+ sections.push("## Create (full)", "", ...entry.create.map(f => `- ${f}`), "");
994
+ }
995
+
996
+ if (entry.update?.length) {
997
+ sections.push("## Update", "", ...entry.update.map(f => `- ${f}`), "");
998
+ }
999
+
1000
+ if (entry.minimum_config?.length) {
1001
+ sections.push("## Minimum Configuration", "", ...entry.minimum_config.map(f => `- ${f}`), "");
1002
+ }
1003
+
1004
+ return sections.join("\n");
1005
+ }
1006
+
820
1007
  function renderUnknownDomain(requested: string, index: ApiSpecIndex): string {
821
1008
  const suggestions = index.domains
822
1009
  .filter(d => d.domain.includes(requested) || requested.includes(d.domain.slice(0, 3)))
@@ -177,8 +177,19 @@ export interface ApiSpecOperationEnrichment {
177
177
  };
178
178
  }
179
179
 
180
+ export interface ApiSpecMinimumConfiguration {
181
+ readonly required_fields: readonly string[];
182
+ readonly example_yaml?: string;
183
+ readonly example_json?: string;
184
+ readonly example_curl?: string;
185
+ readonly description?: string;
186
+ readonly mutually_exclusive_groups?: readonly unknown[];
187
+ readonly [key: string]: unknown;
188
+ }
189
+
180
190
  export interface ApiSpecSchemaEnrichment {
181
191
  readonly recommendedOneofVariant?: Readonly<Record<string, string>>;
192
+ readonly minimumConfiguration?: ApiSpecMinimumConfiguration;
182
193
  }
183
194
 
184
195
  export interface ApiSpecDomainEnrichments {
@@ -186,6 +197,13 @@ export interface ApiSpecDomainEnrichments {
186
197
  readonly schemaEnrichments: Readonly<Record<string, ApiSpecSchemaEnrichment>>;
187
198
  }
188
199
 
200
+ export interface ApiSpecValidationResourceEntry {
201
+ readonly create?: readonly string[];
202
+ readonly update?: readonly string[];
203
+ readonly minimum_config?: readonly string[];
204
+ readonly [key: string]: unknown;
205
+ }
206
+
189
207
  export interface ApiSpecIndex {
190
208
  readonly version: string;
191
209
  readonly timestamp: string;
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "18.72.0",
21
- "commit": "ee865e583dfa21db5c5f5bf07001570c5f7f9086",
22
- "shortCommit": "ee865e5",
20
+ "version": "18.77.1",
21
+ "commit": "892a2fd604ed3fe5dece27147eb4d9fb4d968450",
22
+ "shortCommit": "892a2fd",
23
23
  "branch": "main",
24
- "tag": "v18.72.0",
25
- "commitDate": "2026-05-19T19:45:52Z",
26
- "buildDate": "2026-05-19T20:24:28.090Z",
27
- "dirty": false,
24
+ "tag": "v18.77.1",
25
+ "commitDate": "2026-05-23T17:07:44-04:00",
26
+ "buildDate": "2026-05-23T22:08:01.020Z",
27
+ "dirty": true,
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/ee865e583dfa21db5c5f5bf07001570c5f7f9086",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.72.0"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/892a2fd604ed3fe5dece27147eb4d9fb4d968450",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.77.1"
33
33
  };
@@ -25,7 +25,12 @@ import type { ContextStatus } from "../services/f5xc-context";
25
25
  import { type ApiCatalogResolver, createApiCatalogResolver } from "./api-catalog-resolve";
26
26
  import type { ApiCatalogCategory, ApiCatalogCategorySummary, ApiCatalogIndex } from "./api-catalog-types";
27
27
  import { type ApiSpecResolver, createApiSpecResolver } from "./api-spec-resolve";
28
- import type { ApiSpecDomainEnrichments, ApiSpecIndex, OpenAPISpec } from "./api-spec-types";
28
+ import type {
29
+ ApiSpecDomainEnrichments,
30
+ ApiSpecIndex,
31
+ ApiSpecValidationResourceEntry,
32
+ OpenAPISpec,
33
+ } from "./api-spec-types";
29
34
  import { getRuntimeBuildInfo, type RuntimeBuildInfo, renderAboutDoc } from "./build-info-runtime";
30
35
  import { loadComputerProfile, renderComputerProfileMarkdown, seedComputerProfile } from "./computer-profile";
31
36
  import { EMBEDDED_DOC_FILENAMES, EMBEDDED_DOCS } from "./docs-index.generated";
@@ -55,6 +60,7 @@ let _apiSpecCache: {
55
60
  index: ApiSpecIndex;
56
61
  data: Readonly<Record<string, OpenAPISpec>>;
57
62
  enrichments: Readonly<Record<string, ApiSpecDomainEnrichments>>;
63
+ validationData: Readonly<Record<string, ApiSpecValidationResourceEntry>>;
58
64
  version: string;
59
65
  } | null = null;
60
66
 
@@ -62,6 +68,7 @@ function loadApiSpecs(): {
62
68
  index: ApiSpecIndex;
63
69
  data: Readonly<Record<string, OpenAPISpec>>;
64
70
  enrichments: Readonly<Record<string, ApiSpecDomainEnrichments>>;
71
+ validationData: Readonly<Record<string, ApiSpecValidationResourceEntry>>;
65
72
  version: string;
66
73
  } {
67
74
  if (_apiSpecCache) return _apiSpecCache;
@@ -71,6 +78,7 @@ function loadApiSpecs(): {
71
78
  API_SPEC_DATA?: Readonly<Record<string, unknown>>;
72
79
  API_SPEC_VERSION?: string;
73
80
  API_SPEC_ENRICHMENTS?: Readonly<Record<string, ApiSpecDomainEnrichments>>;
81
+ API_VALIDATION_DATA?: Readonly<Record<string, ApiSpecValidationResourceEntry>>;
74
82
  };
75
83
  const index = mod.API_SPEC_INDEX ?? EMPTY_INDEX;
76
84
  const version = mod.API_SPEC_VERSION ?? "unknown";
@@ -81,13 +89,14 @@ function loadApiSpecs(): {
81
89
  index,
82
90
  data: (mod.API_SPEC_DATA ?? {}) as Readonly<Record<string, OpenAPISpec>>,
83
91
  enrichments: mod.API_SPEC_ENRICHMENTS ?? {},
92
+ validationData: (mod.API_VALIDATION_DATA ?? {}) as Readonly<Record<string, ApiSpecValidationResourceEntry>>,
84
93
  version,
85
94
  };
86
95
  } catch (err) {
87
96
  logger.warn("api-spec index unavailable, embedded specs disabled", {
88
97
  error: err instanceof Error ? err.message : String(err),
89
98
  });
90
- _apiSpecCache = { index: EMPTY_INDEX, data: {}, enrichments: {}, version: "unavailable" };
99
+ _apiSpecCache = { index: EMPTY_INDEX, data: {}, enrichments: {}, validationData: {}, version: "unavailable" };
91
100
  }
92
101
  return _apiSpecCache;
93
102
  }
@@ -153,7 +162,12 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
153
162
  #getApiSpecResolver(): ApiSpecResolver {
154
163
  if (!this.#apiSpecResolver) {
155
164
  const specs = loadApiSpecs();
156
- this.#apiSpecResolver = createApiSpecResolver(specs.index, specs.data, specs.enrichments);
165
+ this.#apiSpecResolver = createApiSpecResolver(
166
+ specs.index,
167
+ specs.data,
168
+ specs.enrichments,
169
+ specs.validationData,
170
+ );
157
171
  }
158
172
  return this.#apiSpecResolver;
159
173
  }