@f5xc-salesdemos/xcsh 18.32.0 → 18.33.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.32.0",
4
+ "version": "18.33.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",
@@ -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.32.0",
52
- "@f5xc-salesdemos/pi-agent-core": "18.32.0",
53
- "@f5xc-salesdemos/pi-ai": "18.32.0",
54
- "@f5xc-salesdemos/pi-natives": "18.32.0",
55
- "@f5xc-salesdemos/pi-tui": "18.32.0",
56
- "@f5xc-salesdemos/pi-utils": "18.32.0",
51
+ "@f5xc-salesdemos/xcsh-stats": "18.33.0",
52
+ "@f5xc-salesdemos/pi-agent-core": "18.33.0",
53
+ "@f5xc-salesdemos/pi-ai": "18.33.0",
54
+ "@f5xc-salesdemos/pi-natives": "18.33.0",
55
+ "@f5xc-salesdemos/pi-tui": "18.33.0",
56
+ "@f5xc-salesdemos/pi-utils": "18.33.0",
57
57
  "@sinclair/typebox": "^0.34",
58
58
  "@xterm/headless": "^6.0",
59
59
  "ajv": "^8.18",
@@ -187,6 +187,21 @@ if (!fs.existsSync(indexPath)) {
187
187
 
188
188
  const rawIndex: RawIndex = JSON.parse(fs.readFileSync(indexPath, "utf-8"));
189
189
 
190
+ const catalog = await downloadCatalog(specsDir);
191
+
192
+ const pathToCatalogCategories = new Map<string, string[]>();
193
+ if (catalog) {
194
+ const cats = (catalog.categories ?? []) as Array<{ name: string; operations: Array<{ path: string }> }>;
195
+ for (const cat of cats) {
196
+ for (const op of cat.operations ?? []) {
197
+ if (!op.path) continue;
198
+ const existing = pathToCatalogCategories.get(op.path) ?? [];
199
+ existing.push(cat.name);
200
+ pathToCatalogCategories.set(op.path, existing);
201
+ }
202
+ }
203
+ }
204
+
190
205
  const domainEntries: string[] = [];
191
206
  const blobEntries: string[] = [];
192
207
  let processedCount = 0;
@@ -224,6 +239,13 @@ for (const entry of rawIndex.specifications) {
224
239
  fields.push(`dependencies: ${JSON.stringify(r.dependencies)}`);
225
240
  }
226
241
  if (r.relationship_hints?.length) fields.push(`relationshipHints: ${JSON.stringify(r.relationship_hints)}`);
242
+ const catalogCats = new Set<string>();
243
+ for (const ap of r.api_paths ?? []) {
244
+ for (const cat of pathToCatalogCategories.get(ap) ?? []) {
245
+ catalogCats.add(cat);
246
+ }
247
+ }
248
+ if (catalogCats.size > 0) fields.push(`catalogCategories: ${JSON.stringify([...catalogCats])}`);
227
249
  return `\t\t\t{ ${fields.join(", ")} },`;
228
250
  });
229
251
 
@@ -304,7 +326,6 @@ console.log(
304
326
  );
305
327
 
306
328
  // Generate API catalog index
307
- const catalog = await downloadCatalog(specsDir);
308
329
  if (catalog) {
309
330
  const categories = (catalog.categories ?? []) as Array<{ name: string; displayName: string; operations: unknown[] }>;
310
331
  const catalogBlobEntries: string[] = [];
@@ -1,10 +1,15 @@
1
1
  import { gunzipSync } from "node:zlib";
2
2
  import { LRUCache } from "lru-cache";
3
3
  import type { ApiCatalogCategory, ApiCatalogCategorySummary, ApiCatalogIndex } from "./api-catalog-types";
4
+ import type { ApiSpecIndex } from "./api-spec-types";
4
5
  import type { InternalResource, InternalUrl } from "./types";
5
6
 
6
7
  const LRU_CAPACITY = 5;
7
8
 
9
+ function normalizeSearchTerm(s: string): string {
10
+ return s.toLowerCase().replace(/_/g, "-");
11
+ }
12
+
8
13
  export interface ApiCatalogResolver {
9
14
  resolve(url: InternalUrl): Promise<InternalResource>;
10
15
  }
@@ -13,6 +18,7 @@ export function createApiCatalogResolver(
13
18
  index: ApiCatalogIndex,
14
19
  categorySummaries: readonly ApiCatalogCategorySummary[],
15
20
  blobs: Record<string, string>,
21
+ specIndex?: ApiSpecIndex,
16
22
  ): ApiCatalogResolver {
17
23
  const cache = new LRUCache<string, ApiCatalogCategory>({ max: LRU_CAPACITY });
18
24
 
@@ -35,8 +41,27 @@ export function createApiCatalogResolver(
35
41
  const pathname = url.rawPathname ?? url.pathname;
36
42
  const category = pathname.replace(/^\//, "").replace(/\/$/, "");
37
43
  const search = url.searchParams.get("search");
44
+ const resourceName = url.searchParams.get("resource");
38
45
 
39
46
  if (!category) {
47
+ if (resourceName && specIndex) {
48
+ for (const domain of specIndex.domains) {
49
+ const res = domain.resources.find(r => r.name === resourceName);
50
+ if (res?.catalogCategories?.length) {
51
+ const catName = res.catalogCategories[0];
52
+ if (categorySummaries.some(c => c.name === catName)) {
53
+ try {
54
+ const cat = decompress(catName);
55
+ return makeResource(url, renderCatalogDetail(cat, index));
56
+ } catch {
57
+ break;
58
+ }
59
+ }
60
+ }
61
+ }
62
+ return makeResource(url, renderCatalogSearch(index, categorySummaries, resourceName));
63
+ }
64
+
40
65
  const content = search
41
66
  ? renderCatalogSearch(index, categorySummaries, search)
42
67
  : renderCatalogIndex(index, categorySummaries);
@@ -89,9 +114,9 @@ function renderCatalogSearch(
89
114
  summaries: readonly ApiCatalogCategorySummary[],
90
115
  term: string,
91
116
  ): string {
92
- const lower = term.toLowerCase();
117
+ const normalized = normalizeSearchTerm(term);
93
118
  const matches = summaries.filter(
94
- c => c.name.toLowerCase().includes(lower) || c.displayName.toLowerCase().includes(lower),
119
+ c => normalizeSearchTerm(c.name).includes(normalized) || normalizeSearchTerm(c.displayName).includes(normalized),
95
120
  );
96
121
 
97
122
  if (matches.length === 0) {
@@ -117,6 +142,39 @@ function renderCatalogSearch(
117
142
  ].join("\n");
118
143
  }
119
144
 
145
+ function formatConstraints(constraints: Record<string, unknown> | undefined): string {
146
+ if (!constraints) return "--";
147
+ const parts: string[] = [];
148
+ if (constraints.pattern) parts.push(`pattern: \`${constraints.pattern}\``);
149
+ if (constraints.maxLength != null) parts.push(`maxLength: ${constraints.maxLength}`);
150
+ if (constraints.minLength != null) parts.push(`minLength: ${constraints.minLength}`);
151
+ if (constraints.minimum != null) parts.push(`min: ${constraints.minimum}`);
152
+ if (constraints.maximum != null) parts.push(`max: ${constraints.maximum}`);
153
+ if (constraints.minItems != null) parts.push(`minItems: ${constraints.minItems}`);
154
+ if (constraints.maxItems != null) parts.push(`maxItems: ${constraints.maxItems}`);
155
+ if (constraints.format) parts.push(`format: ${constraints.format}`);
156
+ if (Array.isArray(constraints.enum)) parts.push(`enum: ${constraints.enum.join(", ")}`);
157
+ return parts.length > 0 ? parts.join(", ") : "--";
158
+ }
159
+
160
+ function formatRequiredFor(
161
+ reqFor: { minimum_config?: boolean; create?: boolean; update?: boolean; read?: boolean } | undefined,
162
+ ): string {
163
+ if (!reqFor) return "--";
164
+ const ops: string[] = [];
165
+ if (reqFor.minimum_config) ops.push("minimum_config");
166
+ if (reqFor.create) ops.push("create");
167
+ if (reqFor.update) ops.push("update");
168
+ if (reqFor.read) ops.push("read");
169
+ return ops.length > 0 ? ops.join(", ") : "--";
170
+ }
171
+
172
+ function formatDefault(defaultVal: unknown, serverDefault: boolean | undefined): string {
173
+ if (defaultVal == null && !serverDefault) return "--";
174
+ const val = defaultVal != null ? String(defaultVal) : "--";
175
+ return serverDefault ? `${val} (server)` : val;
176
+ }
177
+
120
178
  function renderCatalogDetail(cat: ApiCatalogCategory, index: ApiCatalogIndex): string {
121
179
  const sections: string[] = [`# ${cat.displayName}`, "", `${cat.operations.length} operations.`];
122
180
 
@@ -134,7 +192,7 @@ function renderCatalogDetail(cat: ApiCatalogCategory, index: ApiCatalogIndex): s
134
192
  }
135
193
  }
136
194
 
137
- if (op.bodySchema) {
195
+ if (!op.minimumPayload && op.bodySchema) {
138
196
  const minConfig = (op.bodySchema as Record<string, unknown>)["x-f5xc-minimum-configuration"] as
139
197
  | Record<string, unknown>
140
198
  | undefined;
@@ -157,6 +215,47 @@ function renderCatalogDetail(cat: ApiCatalogCategory, index: ApiCatalogIndex): s
157
215
  sections[sections.length - 1] = lastLine.replace(/ \\$/, "");
158
216
  }
159
217
  sections.push("```");
218
+
219
+ // Minimum Configuration (Tier 1)
220
+ if (op.minimumPayload) {
221
+ sections.push("", "### Minimum Configuration", "");
222
+ sections.push(`Required fields: ${op.minimumPayload.requiredFields.join(", ")}`);
223
+ sections.push("", "```json", JSON.stringify(op.minimumPayload.json, null, 2), "```");
224
+ }
225
+
226
+ // Field Constraints (Tier 2)
227
+ if (op.fieldMetadata && Object.keys(op.fieldMetadata).length > 0) {
228
+ sections.push("", "### Field Constraints", "");
229
+ sections.push("| Field | Type | Description | Constraint | Required For | Default |");
230
+ sections.push("|-------|------|-------------|-----------|--------------|---------|");
231
+ for (const [field, meta] of Object.entries(op.fieldMetadata)) {
232
+ const desc = meta.description ?? "";
233
+ const constraint = formatConstraints(meta.constraints);
234
+ const reqFor = formatRequiredFor(meta.required_for);
235
+ const def = formatDefault(meta.default, meta.serverDefault);
236
+ sections.push(`| ${field} | ${meta.type} | ${desc} | ${constraint} | ${reqFor} | ${def} |`);
237
+ }
238
+ }
239
+
240
+ // OneOf Recommendations
241
+ if (op.oneOfRecommendations && Object.keys(op.oneOfRecommendations).length > 0) {
242
+ sections.push("", "### OneOf Recommendations", "");
243
+ sections.push("| Path | Recommended Variant |");
244
+ sections.push("|------|-------------------|");
245
+ for (const [path, variant] of Object.entries(op.oneOfRecommendations)) {
246
+ sections.push(`| ${path} | ${variant} |`);
247
+ }
248
+ }
249
+
250
+ // Response Summary
251
+ if (op.responseSummary && op.responseSummary.length > 0) {
252
+ sections.push("", "### Response", "");
253
+ sections.push("| Field | Type | Description |");
254
+ sections.push("|-------|------|-------------|");
255
+ for (const f of op.responseSummary) {
256
+ sections.push(`| ${f.field} | ${f.type} | ${f.description} |`);
257
+ }
258
+ }
160
259
  }
161
260
 
162
261
  sections.push("");
@@ -165,7 +264,11 @@ function renderCatalogDetail(cat: ApiCatalogCategory, index: ApiCatalogIndex): s
165
264
 
166
265
  function renderUnknownCategory(requested: string, summaries: readonly ApiCatalogCategorySummary[]): string {
167
266
  const suggestions = summaries
168
- .filter(c => c.name.includes(requested) || requested.includes(c.name.slice(0, 4)))
267
+ .filter(c => {
268
+ const norm = normalizeSearchTerm(requested);
269
+ const normName = normalizeSearchTerm(c.name);
270
+ return normName.includes(norm) || norm.includes(normName.slice(0, 4));
271
+ })
169
272
  .slice(0, 5);
170
273
 
171
274
  const sections = [`# Category not found: ${requested}`, ""];
@@ -13,6 +13,34 @@ export interface ApiCatalogParameter {
13
13
  readonly default?: string;
14
14
  }
15
15
 
16
+ export interface ApiCatalogMinimumPayload {
17
+ readonly json: Record<string, unknown>;
18
+ readonly requiredFields: readonly string[];
19
+ readonly description: string;
20
+ }
21
+
22
+ export interface ApiCatalogFieldMeta {
23
+ readonly type: string;
24
+ readonly description?: string;
25
+ readonly required_for?: {
26
+ readonly minimum_config?: boolean;
27
+ readonly create?: boolean;
28
+ readonly update?: boolean;
29
+ readonly read?: boolean;
30
+ };
31
+ readonly serverDefault?: boolean;
32
+ readonly default?: unknown;
33
+ readonly recommendedValue?: unknown;
34
+ readonly constraints?: Record<string, unknown>;
35
+ readonly conflictsWith?: readonly string[];
36
+ }
37
+
38
+ export interface ApiCatalogResponseField {
39
+ readonly field: string;
40
+ readonly type: string;
41
+ readonly description: string;
42
+ }
43
+
16
44
  export interface ApiCatalogOperation {
17
45
  readonly name: string;
18
46
  readonly description: string;
@@ -22,6 +50,10 @@ export interface ApiCatalogOperation {
22
50
  readonly parameters: readonly ApiCatalogParameter[];
23
51
  readonly bodySchema?: Record<string, unknown>;
24
52
  readonly responseSchema?: Record<string, unknown>;
53
+ readonly minimumPayload?: ApiCatalogMinimumPayload;
54
+ readonly fieldMetadata?: Readonly<Record<string, ApiCatalogFieldMeta>>;
55
+ readonly oneOfRecommendations?: Readonly<Record<string, string>>;
56
+ readonly responseSummary?: readonly ApiCatalogResponseField[];
25
57
  }
26
58
 
27
59
  export interface ApiCatalogCategory {
@@ -5,6 +5,7 @@ import type { InternalResource, InternalUrl } from "./types";
5
5
 
6
6
  const LRU_CAPACITY = 5;
7
7
  const SCHEMA_RENDER_MAX_DEPTH = 3;
8
+ const CRUD_OPERATION_SUFFIXES = [".API.Create", ".API.Replace", ".API.Get", ".API.List", ".API.Delete"];
8
9
 
9
10
  const groupsCache = new WeakMap<OpenAPISpec, Map<string, Record<string, Record<string, OpenAPIPathOperation>>>>();
10
11
 
@@ -78,12 +79,13 @@ export function createApiSpecResolver(index: ApiSpecIndex, blobs: Record<string,
78
79
  const pathFilter = url.searchParams.get("path");
79
80
 
80
81
  if (resource) {
82
+ const crud = url.searchParams.get("crud") === "true";
81
83
  const spec = decompress(domain);
82
84
  const matchingPaths = filterPathsByResource(spec, resource, entry);
83
85
  if (Object.keys(matchingPaths).length === 0) {
84
86
  return makeResource(url, renderUnknownResource(resource, entry, spec));
85
87
  }
86
- return makeResource(url, renderResourceSpec(domain, resource, spec, entry));
88
+ return makeResource(url, renderResourceSpec(domain, resource, spec, entry, { crudOnly: crud }));
87
89
  }
88
90
 
89
91
  if (pathFilter) {
@@ -306,13 +308,24 @@ function filterPathsByResource(
306
308
  return result;
307
309
  }
308
310
 
309
- function renderResourceSpec(_domain: string, resource: string, spec: OpenAPISpec, entry?: ApiSpecDomainEntry): string {
311
+ function renderResourceSpec(
312
+ _domain: string,
313
+ resource: string,
314
+ spec: OpenAPISpec,
315
+ entry?: ApiSpecDomainEntry,
316
+ options?: { crudOnly?: boolean },
317
+ ): string {
310
318
  const matchingPaths = filterPathsByResource(spec, resource, entry);
311
- const sections = [`# ${resource} Full API Specification`, ""];
319
+ const label = options?.crudOnly ? "CRUD Operations" : "Full API Specification";
320
+ const sections = [`# ${resource} — ${label}`, ""];
312
321
 
313
322
  for (const [pathKey, methods] of Object.entries(matchingPaths)) {
314
323
  for (const [method, op] of Object.entries(methods)) {
315
324
  if (typeof op !== "object" || !op) continue;
325
+ if (options?.crudOnly) {
326
+ const opId = op.operationId ?? "";
327
+ if (!CRUD_OPERATION_SUFFIXES.some(s => opId.endsWith(s))) continue;
328
+ }
316
329
  const operation = op;
317
330
  sections.push(`## ${method.toUpperCase()} ${pathKey}`, "");
318
331
  if (operation.summary) sections.push(String(operation.summary), "");
@@ -20,6 +20,7 @@ export interface ApiSpecDomainResource {
20
20
  readonly optional: readonly string[];
21
21
  };
22
22
  readonly relationshipHints?: readonly string[];
23
+ readonly catalogCategories?: readonly string[];
23
24
  }
24
25
 
25
26
  export interface ApiSpecDomainEntry {
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "18.32.0",
21
- "commit": "c5505c8b32a0441554d88d83cded48c25b110135",
22
- "shortCommit": "c5505c8",
20
+ "version": "18.33.0",
21
+ "commit": "4c91daabbadd70bca20cf37d69efd0ef0f52a8bf",
22
+ "shortCommit": "4c91daa",
23
23
  "branch": "main",
24
- "tag": "v18.32.0",
25
- "commitDate": "2026-05-02T02:44:42Z",
26
- "buildDate": "2026-05-02T03:10:54.675Z",
24
+ "tag": "v18.33.0",
25
+ "commitDate": "2026-05-02T21:09:17Z",
26
+ "buildDate": "2026-05-02T21:28:48.472Z",
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/c5505c8b32a0441554d88d83cded48c25b110135",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.32.0"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/4c91daabbadd70bca20cf37d69efd0ef0f52a8bf",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.33.0"
33
33
  };
@@ -141,7 +141,13 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
141
141
  #getApiCatalogResolver(): ApiCatalogResolver {
142
142
  if (!this.#apiCatalogResolver) {
143
143
  const catalog = loadApiCatalog();
144
- this.#apiCatalogResolver = createApiCatalogResolver(catalog.index, catalog.summaries, catalog.blobs);
144
+ const specs = loadApiSpecs();
145
+ this.#apiCatalogResolver = createApiCatalogResolver(
146
+ catalog.index,
147
+ catalog.summaries,
148
+ catalog.blobs,
149
+ specs.index,
150
+ );
145
151
  }
146
152
  return this.#apiCatalogResolver;
147
153
  }
@@ -191,20 +191,31 @@ Most tools resolve custom protocol URLs to internal resources (not web URLs):
191
191
  - `xcsh://about` — Identity, version, build fingerprint, architecture, self-improvement. **MUST** read for any question about xcsh before exploring `~/.xcsh/`.
192
192
  This document contains the authoritative repository URL, issues URL, and source location.
193
193
  For identity questions (source code, repo, version, who built this) — answer from `xcsh://about` alone. Do not call external GitHub tools.
194
- - `xcsh://api-spec/` — F5 XC API specifications.
195
- **MUST NOT** read proactively. When the user needs to interact with the F5 XC API:
196
- 1. Read `xcsh://api-spec/` to identify the correct domain
197
- 2. Read `xcsh://api-spec/{domain}` to find the resource and operations
198
- 3. Read `xcsh://api-spec/{domain}?resource={name}` for full endpoint specification
194
+ - `xcsh://api-spec/` — F5 XC API specifications (schema introspection, field types, validation).
195
+ - `xcsh://api-catalog/` F5 XC API operations with curl templates (CRUD execution).
196
+
197
+ When the user needs to **make an API call** (create, read, update, delete):
198
+
199
+ 1. `xcsh://api-catalog/?search={term}` → find the operation
200
+ 2. `xcsh://api-catalog/{category}` → get curl template, minimum payload,
201
+ field constraints, OneOf recommendations, and response summary
202
+
203
+ For POST/PUT operations, the catalog includes a ready-to-use JSON payload.
204
+ Customize the payload with user-specified values (name, namespace, etc.),
205
+ substitute path parameters (`{namespace}`, `{name}`) with actual values,
206
+ insert the payload as the `-d` body in the curl template, and execute.
207
+
208
+ When the user needs to **understand a schema** (field types, nested objects, request body structure):
209
+
210
+ 1. `xcsh://api-spec/{domain}?resource={name}` → full OpenAPI specification
211
+ If the domain is unknown, read `xcsh://api-spec/` first to identify it.
212
+
213
+ **MUST NOT** read proactively.
214
+ Never start at `xcsh://api-spec/` for CRUD operations — it returns the full schema (~40K tokens)
215
+ when the catalog provides the same endpoint with curl template (~700 tokens).
199
216
  Never guess API paths or request schemas.
200
217
  Also available: `xcsh://api-spec/workflows/` (step-by-step guides),
201
- `xcsh://api-spec/errors/{code}` (error resolution),
202
- `xcsh://api-spec/glossary/` (acronym reference).
203
-
204
- - `xcsh://api-catalog/` — Pre-built API operation catalog with curl templates.
205
- **MUST NOT** read proactively. When building API requests:
206
- 1. Read `xcsh://api-catalog/?search={term}` to find the right operation
207
- 2. Read `xcsh://api-catalog/{category}` for full operation details and curl template
218
+ `xcsh://api-spec/errors/{code}` (error resolution), `xcsh://api-spec/glossary/` (acronym reference).
208
219
 
209
220
  In `bash`, URIs auto-resolve to filesystem paths (e.g., `python skill://my-skill/scripts/init.py`).
210
221