@f5xc-salesdemos/xcsh 18.30.1 → 18.30.2
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 +56 -5
- package/src/internal-urls/api-spec-resolve.ts +85 -47
- 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 +6 -0
- package/src/prompts/system/system-prompt.md +2 -2
- package/src/system-prompt.ts +0 -28
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "18.30.
|
|
4
|
+
"version": "18.30.2",
|
|
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.30.
|
|
52
|
-
"@f5xc-salesdemos/pi-agent-core": "18.30.
|
|
53
|
-
"@f5xc-salesdemos/pi-ai": "18.30.
|
|
54
|
-
"@f5xc-salesdemos/pi-natives": "18.30.
|
|
55
|
-
"@f5xc-salesdemos/pi-tui": "18.30.
|
|
56
|
-
"@f5xc-salesdemos/pi-utils": "18.30.
|
|
51
|
+
"@f5xc-salesdemos/xcsh-stats": "18.30.2",
|
|
52
|
+
"@f5xc-salesdemos/pi-agent-core": "18.30.2",
|
|
53
|
+
"@f5xc-salesdemos/pi-ai": "18.30.2",
|
|
54
|
+
"@f5xc-salesdemos/pi-natives": "18.30.2",
|
|
55
|
+
"@f5xc-salesdemos/pi-tui": "18.30.2",
|
|
56
|
+
"@f5xc-salesdemos/pi-utils": "18.30.2",
|
|
57
57
|
"@sinclair/typebox": "^0.34",
|
|
58
58
|
"@xterm/headless": "^6.0",
|
|
59
59
|
"ajv": "^8.18",
|
|
@@ -1,10 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { execFileSync } from "node:child_process";
|
|
4
3
|
import * as fs from "node:fs";
|
|
5
4
|
import * as os from "node:os";
|
|
6
5
|
import * as path from "node:path";
|
|
7
6
|
import { gzipSync } from "node:zlib";
|
|
7
|
+
import { $ } from "bun";
|
|
8
|
+
|
|
9
|
+
interface SpecPathOperation {
|
|
10
|
+
operationId?: string;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Finds the operationId schema components (e.g., 'dns_zone', 'views.forward_proxy_policy')
|
|
16
|
+
* that correspond to a given resource name by matching path segments.
|
|
17
|
+
*/
|
|
18
|
+
function findResourceSchemaComponents(
|
|
19
|
+
resourceName: string,
|
|
20
|
+
paths: Record<string, Record<string, SpecPathOperation>>,
|
|
21
|
+
): string[] {
|
|
22
|
+
const name = resourceName.replace(/-/g, "_");
|
|
23
|
+
const plural = name.endsWith("s") ? name : `${name}s`;
|
|
24
|
+
const found = new Set<string>();
|
|
25
|
+
|
|
26
|
+
for (const [pathKey, methods] of Object.entries(paths)) {
|
|
27
|
+
const segments = pathKey.split("/");
|
|
28
|
+
if (!segments.some(s => s === name || s === plural)) continue;
|
|
29
|
+
|
|
30
|
+
for (const op of Object.values(methods)) {
|
|
31
|
+
const opId = op?.operationId;
|
|
32
|
+
if (!opId) continue;
|
|
33
|
+
const match = opId.match(/^ves\.io\.schema\.(.+?)\.(?:API|CustomAPI)\./);
|
|
34
|
+
if (match) found.add(match[1]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return [...found];
|
|
39
|
+
}
|
|
8
40
|
|
|
9
41
|
interface IndexEntry {
|
|
10
42
|
domain: string;
|
|
@@ -33,6 +65,7 @@ const outputPath = path.resolve(import.meta.dir, "../src/internal-urls/api-spec-
|
|
|
33
65
|
|
|
34
66
|
async function downloadFromRelease(): Promise<string> {
|
|
35
67
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "api-specs-"));
|
|
68
|
+
downloadedTmpDir = tmpDir;
|
|
36
69
|
const tag = process.env.API_SPECS_TAG ?? PINNED_TAG;
|
|
37
70
|
const zipName = `f5xc-api-specs-${tag}.zip`;
|
|
38
71
|
const downloadUrl = `https://github.com/${REPO}/releases/download/${tag}/${zipName}`;
|
|
@@ -49,7 +82,13 @@ async function downloadFromRelease(): Promise<string> {
|
|
|
49
82
|
|
|
50
83
|
const extractDir = path.join(tmpDir, "extracted");
|
|
51
84
|
fs.mkdirSync(extractDir, { recursive: true });
|
|
52
|
-
|
|
85
|
+
const result = await $`unzip -q ${zipPath} -d ${extractDir}`.nothrow();
|
|
86
|
+
if (result.exitCode !== 0) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Failed to extract ${zipPath}: unzip exited with code ${result.exitCode}.\n` +
|
|
89
|
+
"Ensure 'unzip' is installed: apt install unzip / brew install unzip",
|
|
90
|
+
);
|
|
91
|
+
}
|
|
53
92
|
|
|
54
93
|
const domainsDir = path.join(extractDir, "domains");
|
|
55
94
|
if (fs.existsSync(domainsDir) && fs.existsSync(path.join(extractDir, "index.json"))) {
|
|
@@ -75,6 +114,8 @@ async function findSpecsDir(): Promise<string> {
|
|
|
75
114
|
return downloadFromRelease();
|
|
76
115
|
}
|
|
77
116
|
|
|
117
|
+
let downloadedTmpDir: string | null = null;
|
|
118
|
+
|
|
78
119
|
const specsDir = await findSpecsDir();
|
|
79
120
|
console.log(`Reading specs from: ${specsDir}`);
|
|
80
121
|
|
|
@@ -100,12 +141,18 @@ for (const entry of rawIndex.specifications) {
|
|
|
100
141
|
}
|
|
101
142
|
|
|
102
143
|
const specContent = fs.readFileSync(specFile, "utf-8");
|
|
144
|
+
const specJson = JSON.parse(specContent) as {
|
|
145
|
+
paths?: Record<string, Record<string, SpecPathOperation>>;
|
|
146
|
+
[k: string]: unknown;
|
|
147
|
+
};
|
|
103
148
|
const compressed = gzipSync(Buffer.from(specContent));
|
|
104
149
|
const b64 = compressed.toString("base64");
|
|
105
150
|
|
|
106
|
-
const resources = (entry["x-f5xc-primary-resources"] ?? []).map(
|
|
107
|
-
|
|
108
|
-
|
|
151
|
+
const resources = (entry["x-f5xc-primary-resources"] ?? []).map(r => {
|
|
152
|
+
const schemaComponents = findResourceSchemaComponents(r.name, specJson.paths ?? {});
|
|
153
|
+
const scStr = schemaComponents.length > 0 ? `, schemaComponents: ${JSON.stringify(schemaComponents)}` : "";
|
|
154
|
+
return `\t\t\t{ name: ${JSON.stringify(r.name)}, description: ${JSON.stringify(r.description)}${scStr} },`;
|
|
155
|
+
});
|
|
109
156
|
|
|
110
157
|
const useCases = entry["x-f5xc-use-cases"];
|
|
111
158
|
const relatedDomains = entry["x-f5xc-related-domains"];
|
|
@@ -163,3 +210,7 @@ const outputSize = (Buffer.byteLength(output) / 1024 / 1024).toFixed(1);
|
|
|
163
210
|
console.log(
|
|
164
211
|
`Generated ${path.relative(process.cwd(), outputPath)} (${processedCount} domains, ${skippedCount} skipped, ${outputSize} MB)`,
|
|
165
212
|
);
|
|
213
|
+
|
|
214
|
+
if (downloadedTmpDir) {
|
|
215
|
+
fs.rmSync(downloadedTmpDir, { recursive: true, force: true });
|
|
216
|
+
}
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import { gunzipSync } from "node:zlib";
|
|
2
2
|
import { LRUCache } from "lru-cache";
|
|
3
|
-
import type { ApiSpecDomainEntry, ApiSpecIndex, OpenAPISpec } from "./api-spec-types";
|
|
3
|
+
import type { ApiSpecDomainEntry, ApiSpecIndex, OpenAPIPathOperation, OpenAPISpec } from "./api-spec-types";
|
|
4
4
|
import type { InternalResource, InternalUrl } from "./types";
|
|
5
5
|
|
|
6
6
|
const LRU_CAPACITY = 5;
|
|
7
7
|
const SCHEMA_RENDER_MAX_DEPTH = 3;
|
|
8
8
|
|
|
9
|
+
// Module-level cache for groupPathsBySchema results, keyed by spec identity.
|
|
10
|
+
// Different resolver instances create distinct OpenAPISpec objects so there is no cross-resolver leakage.
|
|
11
|
+
const groupsCache = new WeakMap<OpenAPISpec, Map<string, Record<string, Record<string, OpenAPIPathOperation>>>>();
|
|
12
|
+
|
|
13
|
+
function getCachedGroups(spec: OpenAPISpec): Map<string, Record<string, Record<string, OpenAPIPathOperation>>> {
|
|
14
|
+
const cached = groupsCache.get(spec);
|
|
15
|
+
if (cached) return cached;
|
|
16
|
+
const groups = groupPathsBySchema(spec);
|
|
17
|
+
groupsCache.set(spec, groups);
|
|
18
|
+
return groups;
|
|
19
|
+
}
|
|
20
|
+
|
|
9
21
|
export interface ApiSpecResolver {
|
|
10
22
|
resolve(url: InternalUrl): Promise<InternalResource>;
|
|
11
23
|
}
|
|
@@ -22,11 +34,16 @@ export function createApiSpecResolver(index: ApiSpecIndex, blobs: Record<string,
|
|
|
22
34
|
throw new Error(`No spec blob for domain: ${domain}`);
|
|
23
35
|
}
|
|
24
36
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
try {
|
|
38
|
+
const buffer = Buffer.from(blob, "base64");
|
|
39
|
+
const decompressed = gunzipSync(buffer);
|
|
40
|
+
const spec = JSON.parse(decompressed.toString("utf-8")) as OpenAPISpec;
|
|
41
|
+
cache.set(domain, spec);
|
|
42
|
+
return spec;
|
|
43
|
+
} catch (err) {
|
|
44
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
45
|
+
throw new Error(`Failed to decompress spec for domain '${domain}': ${message}`);
|
|
46
|
+
}
|
|
30
47
|
}
|
|
31
48
|
|
|
32
49
|
return {
|
|
@@ -43,25 +60,30 @@ export function createApiSpecResolver(index: ApiSpecIndex, blobs: Record<string,
|
|
|
43
60
|
return makeResource(url, renderUnknownDomain(domain, index));
|
|
44
61
|
}
|
|
45
62
|
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
try {
|
|
64
|
+
const resource = url.searchParams.get("resource");
|
|
65
|
+
const pathFilter = url.searchParams.get("path");
|
|
48
66
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
if (resource) {
|
|
68
|
+
const spec = decompress(domain);
|
|
69
|
+
const matchingPaths = filterPathsByResource(spec, resource, entry);
|
|
70
|
+
if (Object.keys(matchingPaths).length === 0) {
|
|
71
|
+
return makeResource(url, renderUnknownResource(resource, entry, spec));
|
|
72
|
+
}
|
|
73
|
+
return makeResource(url, renderResourceSpec(domain, resource, spec, entry));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (pathFilter) {
|
|
77
|
+
const spec = decompress(domain);
|
|
78
|
+
return makeResource(url, renderPathSpec(domain, pathFilter, spec));
|
|
54
79
|
}
|
|
55
|
-
return makeResource(url, renderResourceSpec(domain, resource, spec));
|
|
56
|
-
}
|
|
57
80
|
|
|
58
|
-
if (pathFilter) {
|
|
59
81
|
const spec = decompress(domain);
|
|
60
|
-
return makeResource(url,
|
|
82
|
+
return makeResource(url, renderDomainDetail(domain, entry, spec));
|
|
83
|
+
} catch (err) {
|
|
84
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
85
|
+
return makeResource(url, `# Error loading ${domain}\n\n${message}\n`);
|
|
61
86
|
}
|
|
62
|
-
|
|
63
|
-
const spec = decompress(domain);
|
|
64
|
-
return makeResource(url, renderDomainDetail(domain, entry, spec));
|
|
65
87
|
},
|
|
66
88
|
};
|
|
67
89
|
}
|
|
@@ -94,20 +116,13 @@ function renderDomainIndex(index: ApiSpecIndex): string {
|
|
|
94
116
|
}
|
|
95
117
|
|
|
96
118
|
function renderDomainDetail(domain: string, entry: ApiSpecDomainEntry, spec: OpenAPISpec): string {
|
|
97
|
-
const
|
|
98
|
-
const schemaNames = [...groups.keys()].sort();
|
|
99
|
-
|
|
100
|
-
const resourceRows = schemaNames.map(s => {
|
|
101
|
-
const paths = groups.get(s)!;
|
|
102
|
-
const opCount = Object.values(paths).reduce((n, m) => n + Object.keys(m).length, 0);
|
|
103
|
-
return `| ${s} | ${opCount} operations |`;
|
|
104
|
-
});
|
|
119
|
+
const resourceRows = entry.resources.map(r => `| ${r.name} | ${r.description} |`);
|
|
105
120
|
|
|
106
121
|
const operationRows: string[] = [];
|
|
107
122
|
for (const [pathKey, methods] of Object.entries(spec.paths)) {
|
|
108
123
|
for (const [method, op] of Object.entries(methods)) {
|
|
109
124
|
if (typeof op !== "object" || !op) continue;
|
|
110
|
-
const summary =
|
|
125
|
+
const summary = op.summary ?? "";
|
|
111
126
|
operationRows.push(`| ${method.toUpperCase()} | ${pathKey} | ${summary} |`);
|
|
112
127
|
}
|
|
113
128
|
}
|
|
@@ -119,8 +134,8 @@ function renderDomainDetail(domain: string, entry: ApiSpecDomainEntry, spec: Ope
|
|
|
119
134
|
"",
|
|
120
135
|
"## Resources",
|
|
121
136
|
"",
|
|
122
|
-
"| Resource |
|
|
123
|
-
"
|
|
137
|
+
"| Resource | Description |",
|
|
138
|
+
"|----------|-------------|",
|
|
124
139
|
...resourceRows,
|
|
125
140
|
"",
|
|
126
141
|
"## Operations",
|
|
@@ -148,13 +163,13 @@ function extractSchemaComponent(operationId: string): string | null {
|
|
|
148
163
|
return match ? match[1] : null;
|
|
149
164
|
}
|
|
150
165
|
|
|
151
|
-
function groupPathsBySchema(spec: OpenAPISpec): Map<string, Record<string, Record<string,
|
|
152
|
-
const groups = new Map<string, Record<string, Record<string,
|
|
166
|
+
function groupPathsBySchema(spec: OpenAPISpec): Map<string, Record<string, Record<string, OpenAPIPathOperation>>> {
|
|
167
|
+
const groups = new Map<string, Record<string, Record<string, OpenAPIPathOperation>>>();
|
|
153
168
|
|
|
154
169
|
for (const [pathKey, methods] of Object.entries(spec.paths)) {
|
|
155
170
|
for (const [method, op] of Object.entries(methods)) {
|
|
156
171
|
if (typeof op !== "object" || !op) continue;
|
|
157
|
-
const opId =
|
|
172
|
+
const opId = op.operationId;
|
|
158
173
|
if (!opId) continue;
|
|
159
174
|
const schema = extractSchemaComponent(opId);
|
|
160
175
|
if (!schema) continue;
|
|
@@ -163,22 +178,45 @@ function groupPathsBySchema(spec: OpenAPISpec): Map<string, Record<string, Recor
|
|
|
163
178
|
}
|
|
164
179
|
const group = groups.get(schema)!;
|
|
165
180
|
if (!group[pathKey]) group[pathKey] = {};
|
|
166
|
-
group[pathKey][method] = op
|
|
181
|
+
group[pathKey][method] = op;
|
|
167
182
|
}
|
|
168
183
|
}
|
|
169
184
|
|
|
170
185
|
return groups;
|
|
171
186
|
}
|
|
172
187
|
|
|
173
|
-
function filterPathsByResource(
|
|
174
|
-
|
|
188
|
+
function filterPathsByResource(
|
|
189
|
+
spec: OpenAPISpec,
|
|
190
|
+
resource: string,
|
|
191
|
+
entry?: ApiSpecDomainEntry,
|
|
192
|
+
): Record<string, Record<string, OpenAPIPathOperation>> {
|
|
193
|
+
// Index-guided lookup: use pre-computed schemaComponents from the enriched index
|
|
194
|
+
if (entry) {
|
|
195
|
+
const indexedResource = entry.resources.find(r => r.name === resource);
|
|
196
|
+
if (indexedResource?.schemaComponents?.length) {
|
|
197
|
+
const groups = getCachedGroups(spec);
|
|
198
|
+
const result: Record<string, Record<string, OpenAPIPathOperation>> = {};
|
|
199
|
+
for (const comp of indexedResource.schemaComponents) {
|
|
200
|
+
const paths = groups.get(comp);
|
|
201
|
+
if (paths) {
|
|
202
|
+
for (const [pathKey, methods] of Object.entries(paths)) {
|
|
203
|
+
if (!result[pathKey]) result[pathKey] = {};
|
|
204
|
+
Object.assign(result[pathKey], methods);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (Object.keys(result).length > 0) return result;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const groups = getCachedGroups(spec);
|
|
175
213
|
|
|
176
214
|
const exactKey = resource.replace(/-/g, "_");
|
|
177
215
|
if (groups.has(exactKey)) {
|
|
178
216
|
return groups.get(exactKey)!;
|
|
179
217
|
}
|
|
180
218
|
|
|
181
|
-
const partial = new Map<string, Record<string, Record<string,
|
|
219
|
+
const partial = new Map<string, Record<string, Record<string, OpenAPIPathOperation>>>();
|
|
182
220
|
for (const [schema, paths] of groups) {
|
|
183
221
|
const schemaEnd = schema.split(".").at(-1) ?? schema;
|
|
184
222
|
if (
|
|
@@ -201,7 +239,7 @@ function filterPathsByResource(spec: OpenAPISpec, resource: string): Record<stri
|
|
|
201
239
|
}
|
|
202
240
|
|
|
203
241
|
const pluralized = exactKey.endsWith("s") ? exactKey : `${exactKey}s`;
|
|
204
|
-
const result: Record<string, Record<string,
|
|
242
|
+
const result: Record<string, Record<string, OpenAPIPathOperation>> = {};
|
|
205
243
|
for (const [pathKey, methods] of Object.entries(spec.paths)) {
|
|
206
244
|
const segments = pathKey.split("/");
|
|
207
245
|
if (segments.some(s => s === pluralized || s === exactKey)) {
|
|
@@ -211,18 +249,18 @@ function filterPathsByResource(spec: OpenAPISpec, resource: string): Record<stri
|
|
|
211
249
|
return result;
|
|
212
250
|
}
|
|
213
251
|
|
|
214
|
-
function renderResourceSpec(_domain: string, resource: string, spec: OpenAPISpec): string {
|
|
215
|
-
const matchingPaths = filterPathsByResource(spec, resource);
|
|
252
|
+
function renderResourceSpec(_domain: string, resource: string, spec: OpenAPISpec, entry?: ApiSpecDomainEntry): string {
|
|
253
|
+
const matchingPaths = filterPathsByResource(spec, resource, entry);
|
|
216
254
|
const sections = [`# ${resource} — Full API Specification`, ""];
|
|
217
255
|
|
|
218
256
|
for (const [pathKey, methods] of Object.entries(matchingPaths)) {
|
|
219
257
|
for (const [method, op] of Object.entries(methods)) {
|
|
220
258
|
if (typeof op !== "object" || !op) continue;
|
|
221
|
-
const operation = op
|
|
259
|
+
const operation = op;
|
|
222
260
|
sections.push(`## ${method.toUpperCase()} ${pathKey}`, "");
|
|
223
261
|
if (operation.summary) sections.push(String(operation.summary), "");
|
|
224
262
|
|
|
225
|
-
const params = operation.parameters
|
|
263
|
+
const params = operation.parameters;
|
|
226
264
|
if (params?.length) {
|
|
227
265
|
sections.push("### Parameters", "");
|
|
228
266
|
sections.push("| Name | In | Required | Type | Description |");
|
|
@@ -236,7 +274,7 @@ function renderResourceSpec(_domain: string, resource: string, spec: OpenAPISpec
|
|
|
236
274
|
sections.push("");
|
|
237
275
|
}
|
|
238
276
|
|
|
239
|
-
const reqBody = operation.requestBody
|
|
277
|
+
const reqBody = operation.requestBody;
|
|
240
278
|
if (reqBody) {
|
|
241
279
|
sections.push("### Request Body", "");
|
|
242
280
|
const content = reqBody.content as Record<string, Record<string, unknown>> | undefined;
|
|
@@ -247,7 +285,7 @@ function renderResourceSpec(_domain: string, resource: string, spec: OpenAPISpec
|
|
|
247
285
|
}
|
|
248
286
|
}
|
|
249
287
|
|
|
250
|
-
const responses = operation.responses
|
|
288
|
+
const responses = operation.responses;
|
|
251
289
|
if (responses) {
|
|
252
290
|
for (const [status, resp] of Object.entries(responses)) {
|
|
253
291
|
if (typeof resp !== "object" || !resp) continue;
|
|
@@ -283,7 +321,7 @@ function renderPathSpec(_domain: string, pathKey: string, spec: OpenAPISpec): st
|
|
|
283
321
|
|
|
284
322
|
for (const [method, op] of Object.entries(methods)) {
|
|
285
323
|
if (typeof op !== "object" || !op) continue;
|
|
286
|
-
const operation = op
|
|
324
|
+
const operation = op;
|
|
287
325
|
sections.push(`## ${method.toUpperCase()}`, "");
|
|
288
326
|
if (operation.summary) sections.push(String(operation.summary), "");
|
|
289
327
|
}
|
|
@@ -358,7 +396,7 @@ function renderUnknownDomain(requested: string, index: ApiSpecIndex): string {
|
|
|
358
396
|
}
|
|
359
397
|
|
|
360
398
|
function renderUnknownResource(requested: string, entry: ApiSpecDomainEntry, spec: OpenAPISpec): string {
|
|
361
|
-
const groups =
|
|
399
|
+
const groups = getCachedGroups(spec);
|
|
362
400
|
const schemaNames = [...groups.keys()].sort();
|
|
363
401
|
|
|
364
402
|
return [
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "18.30.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "18.30.2",
|
|
21
|
+
"commit": "25941ca15e065dd3be59c1814a5ff8871b7af373",
|
|
22
|
+
"shortCommit": "25941ca",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v18.30.
|
|
25
|
-
"commitDate": "2026-05-
|
|
26
|
-
"buildDate": "2026-05-
|
|
24
|
+
"tag": "v18.30.2",
|
|
25
|
+
"commitDate": "2026-05-01T06:03:28Z",
|
|
26
|
+
"buildDate": "2026-05-01T06:27:47.424Z",
|
|
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.30.
|
|
31
|
+
"commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/25941ca15e065dd3be59c1814a5ff8871b7af373",
|
|
32
|
+
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.30.2"
|
|
33
33
|
};
|
|
@@ -28,6 +28,12 @@ const EMPTY_INDEX: ApiSpecIndex = { version: "unknown", timestamp: "", domains:
|
|
|
28
28
|
|
|
29
29
|
let _apiSpecCache: { index: ApiSpecIndex; blobs: Record<string, string>; version: string } | null = null;
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Lazily loads the generated API spec index. Uses require() instead of
|
|
33
|
+
* top-level import because the generated file may not exist in all
|
|
34
|
+
* contexts (tarball install, type-check without build). The try-catch
|
|
35
|
+
* falls back to an empty index so the handler degrades gracefully.
|
|
36
|
+
*/
|
|
31
37
|
function loadApiSpecs(): { index: ApiSpecIndex; blobs: Record<string, string>; version: string } {
|
|
32
38
|
if (_apiSpecCache) return _apiSpecCache;
|
|
33
39
|
try {
|
|
@@ -189,12 +189,12 @@ Most tools resolve custom protocol URLs to internal resources (not web URLs):
|
|
|
189
189
|
- `mcp://<resource-uri>` — MCP resource from a connected server; matched against exact resource URIs first, then RFC 6570 URI templates advertised by connected servers
|
|
190
190
|
- `xcsh://..` — Internal xcsh documentation. **MUST NOT** read unless the user asks about xcsh itself.
|
|
191
191
|
- `xcsh://about` — Identity, version, build fingerprint, architecture, self-improvement. **MUST** read for any question about xcsh before exploring `~/.xcsh/`.
|
|
192
|
-
- `xcsh://api-spec/` — F5
|
|
192
|
+
- `xcsh://api-spec/` — F5 XC API specifications.
|
|
193
193
|
**MUST NOT** read proactively. When the user needs to interact with the F5 XC API:
|
|
194
194
|
1. Read `xcsh://api-spec/` to identify the correct domain
|
|
195
195
|
2. Read `xcsh://api-spec/{domain}` to find the resource and operations
|
|
196
196
|
3. Read `xcsh://api-spec/{domain}?resource={name}` for full endpoint specification
|
|
197
|
-
Never guess API paths or request schemas
|
|
197
|
+
Never guess API paths or request schemas.
|
|
198
198
|
|
|
199
199
|
In `bash`, URIs auto-resolve to filesystem paths (e.g., `python skill://my-skill/scripts/init.py`).
|
|
200
200
|
|
package/src/system-prompt.ts
CHANGED
|
@@ -16,23 +16,6 @@ import { isApplicableToContext, loadSkills, type Skill } from "./extensibility/s
|
|
|
16
16
|
import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
|
|
17
17
|
import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
|
|
18
18
|
|
|
19
|
-
let _apiSpecMeta: { domainCount: number; version: string } | null = null;
|
|
20
|
-
|
|
21
|
-
function getApiSpecMeta(): { domainCount: number; version: string } {
|
|
22
|
-
if (_apiSpecMeta) return _apiSpecMeta;
|
|
23
|
-
try {
|
|
24
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
25
|
-
const mod = require("./internal-urls/api-spec-index.generated");
|
|
26
|
-
_apiSpecMeta = {
|
|
27
|
-
domainCount: mod.API_SPEC_INDEX?.domains?.length ?? 0,
|
|
28
|
-
version: mod.API_SPEC_VERSION ?? "unknown",
|
|
29
|
-
};
|
|
30
|
-
} catch {
|
|
31
|
-
_apiSpecMeta = { domainCount: 0, version: "unknown" };
|
|
32
|
-
}
|
|
33
|
-
return _apiSpecMeta;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
19
|
let _buildMeta: { version: string; repoSlug: string } | null = null;
|
|
37
20
|
|
|
38
21
|
function getBuildMeta(): { version: string; repoSlug: string } {
|
|
@@ -49,15 +32,6 @@ function getBuildMeta(): { version: string; repoSlug: string } {
|
|
|
49
32
|
}
|
|
50
33
|
return _buildMeta;
|
|
51
34
|
}
|
|
52
|
-
|
|
53
|
-
function apiSpecDomainCount(): number {
|
|
54
|
-
return getApiSpecMeta().domainCount;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function apiSpecVersion(): string {
|
|
58
|
-
return getApiSpecMeta().version;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
35
|
interface AlwaysApplyRule {
|
|
62
36
|
name: string;
|
|
63
37
|
content: string;
|
|
@@ -677,8 +651,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
677
651
|
secretsEnabled,
|
|
678
652
|
context,
|
|
679
653
|
knowledgeTopics: options.knowledgeTopics,
|
|
680
|
-
apiSpecDomainCount: apiSpecDomainCount(),
|
|
681
|
-
apiSpecVersion: apiSpecVersion(),
|
|
682
654
|
};
|
|
683
655
|
let rendered = prompt.render(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
|
|
684
656
|
|