@f5xc-salesdemos/xcsh 18.75.3 → 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/package.json +7 -7
- package/scripts/generate-api-spec-index.ts +44 -4
- package/src/config/settings-schema.ts +10 -0
- package/src/internal-urls/api-spec-resolve.ts +191 -4
- package/src/internal-urls/api-spec-types.ts +18 -0
- package/src/internal-urls/build-info.generated.ts +9 -9
- package/src/internal-urls/xcsh-protocol.ts +17 -3
- package/src/modes/controllers/event-controller.ts +8 -3
- package/src/modes/utils/ui-helpers.ts +3 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "18.
|
|
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": "
|
|
52
|
-
"@f5xc-salesdemos/pi-agent-core": "
|
|
53
|
-
"@f5xc-salesdemos/pi-ai": "
|
|
54
|
-
"@f5xc-salesdemos/pi-natives": "
|
|
55
|
-
"@f5xc-salesdemos/pi-tui": "
|
|
56
|
-
"@f5xc-salesdemos/pi-utils": "
|
|
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
|
-
|
|
445
|
-
|
|
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(
|
|
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
|
|
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.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "18.77.1",
|
|
21
|
+
"commit": "892a2fd604ed3fe5dece27147eb4d9fb4d968450",
|
|
22
|
+
"shortCommit": "892a2fd",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v18.
|
|
25
|
-
"commitDate": "2026-05-
|
|
26
|
-
"buildDate": "2026-05-
|
|
27
|
-
"dirty":
|
|
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/
|
|
32
|
-
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.
|
|
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 {
|
|
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(
|
|
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
|
}
|
|
@@ -338,6 +338,9 @@ export class EventController {
|
|
|
338
338
|
}
|
|
339
339
|
|
|
340
340
|
this.#resetReadGroup();
|
|
341
|
+
if (event.toolName === "todo_write" && !settings.get("todo.verbose")) {
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
341
344
|
const tool = this.ctx.session.getToolByName(event.toolName);
|
|
342
345
|
const component = new ToolExecutionComponent(
|
|
343
346
|
event.toolName,
|
|
@@ -655,9 +658,11 @@ export class EventController {
|
|
|
655
658
|
}
|
|
656
659
|
|
|
657
660
|
case "todo_reminder": {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
+
if (settings.get("todo.verbose")) {
|
|
662
|
+
const component = new TodoReminderComponent(event.todos, event.attempt, event.maxAttempts);
|
|
663
|
+
this.ctx.chatContainer.addChild(createSystemGutter(this.ctx.ui, component));
|
|
664
|
+
this.ctx.ui.requestRender();
|
|
665
|
+
}
|
|
661
666
|
break;
|
|
662
667
|
}
|
|
663
668
|
|
|
@@ -333,6 +333,9 @@ export class UiHelpers {
|
|
|
333
333
|
|
|
334
334
|
// Non-read tool call breaks the group.
|
|
335
335
|
finalizeReadGroup();
|
|
336
|
+
if (content.name === "todo_write" && !settings.get("todo.verbose")) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
336
339
|
const tool = this.ctx.session.getToolByName(content.name);
|
|
337
340
|
const renderArgs =
|
|
338
341
|
"partialJson" in content
|