@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 +7 -7
- package/scripts/generate-api-spec-index.ts +22 -1
- package/src/internal-urls/api-catalog-resolve.ts +32 -3
- package/src/internal-urls/api-spec-resolve.ts +16 -3
- package/src/internal-urls/api-spec-types.ts +1 -0
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/xcsh-protocol.ts +7 -1
- package/src/prompts/system/system-prompt.md +17 -12
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "18.32.
|
|
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.
|
|
52
|
-
"@f5xc-salesdemos/pi-agent-core": "18.32.
|
|
53
|
-
"@f5xc-salesdemos/pi-ai": "18.32.
|
|
54
|
-
"@f5xc-salesdemos/pi-natives": "18.32.
|
|
55
|
-
"@f5xc-salesdemos/pi-tui": "18.32.
|
|
56
|
-
"@f5xc-salesdemos/pi-utils": "18.32.
|
|
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
|
|
117
|
+
const normalized = normalizeSearchTerm(term);
|
|
93
118
|
const matches = summaries.filter(
|
|
94
|
-
c => c.name
|
|
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 =>
|
|
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(
|
|
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
|
|
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), "");
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "18.32.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "18.32.1",
|
|
21
|
+
"commit": "be6f85d9f84378312f0a305e6bf9ace1ea232b6b",
|
|
22
|
+
"shortCommit": "be6f85d",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v18.32.
|
|
25
|
-
"commitDate": "2026-05-
|
|
26
|
-
"buildDate": "2026-05-
|
|
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/
|
|
32
|
-
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.32.
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|