@f5xc-salesdemos/xcsh 18.32.0 → 18.32.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 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.32.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.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.32.1",
52
+ "@f5xc-salesdemos/pi-agent-core": "18.32.1",
53
+ "@f5xc-salesdemos/pi-ai": "18.32.1",
54
+ "@f5xc-salesdemos/pi-natives": "18.32.1",
55
+ "@f5xc-salesdemos/pi-tui": "18.32.1",
56
+ "@f5xc-salesdemos/pi-utils": "18.32.1",
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) {
@@ -165,7 +190,11 @@ function renderCatalogDetail(cat: ApiCatalogCategory, index: ApiCatalogIndex): s
165
190
 
166
191
  function renderUnknownCategory(requested: string, summaries: readonly ApiCatalogCategorySummary[]): string {
167
192
  const suggestions = summaries
168
- .filter(c => c.name.includes(requested) || requested.includes(c.name.slice(0, 4)))
193
+ .filter(c => {
194
+ const norm = normalizeSearchTerm(requested);
195
+ const normName = normalizeSearchTerm(c.name);
196
+ return normName.includes(norm) || norm.includes(normName.slice(0, 4));
197
+ })
169
198
  .slice(0, 5);
170
199
 
171
200
  const sections = [`# Category not found: ${requested}`, ""];
@@ -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.32.1",
21
+ "commit": "be6f85d9f84378312f0a305e6bf9ace1ea232b6b",
22
+ "shortCommit": "be6f85d",
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.32.1",
25
+ "commitDate": "2026-05-02T14:48:30Z",
26
+ "buildDate": "2026-05-02T15:13:51.949Z",
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/be6f85d9f84378312f0a305e6bf9ace1ea232b6b",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.32.1"
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,25 @@ 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 path, method, parameters, curl template
201
+
202
+ When the user needs to **understand a schema** (field types, nested objects, request body structure):
203
+
204
+ 1. `xcsh://api-spec/{domain}?resource={name}` → full OpenAPI specification
205
+ If the domain is unknown, read `xcsh://api-spec/` first to identify it.
206
+
207
+ **MUST NOT** read proactively.
208
+ Never start at `xcsh://api-spec/` for CRUD operations — it returns the full schema (~40K tokens)
209
+ when the catalog provides the same endpoint with curl template (~700 tokens).
199
210
  Never guess API paths or request schemas.
200
211
  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
212
+ `xcsh://api-spec/errors/{code}` (error resolution), `xcsh://api-spec/glossary/` (acronym reference).
208
213
 
209
214
  In `bash`, URIs auto-resolve to filesystem paths (e.g., `python skill://my-skill/scripts/init.py`).
210
215