@f5xc-salesdemos/xcsh 18.30.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5xc-salesdemos/xcsh",
4
- "version": "18.30.0",
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.0",
52
- "@f5xc-salesdemos/pi-agent-core": "18.30.0",
53
- "@f5xc-salesdemos/pi-ai": "18.30.0",
54
- "@f5xc-salesdemos/pi-natives": "18.30.0",
55
- "@f5xc-salesdemos/pi-tui": "18.30.0",
56
- "@f5xc-salesdemos/pi-utils": "18.30.0",
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
- execFileSync("unzip", ["-q", zipPath, "-d", extractDir], { stdio: "inherit" });
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
- r => `\t\t\t{ name: ${JSON.stringify(r.name)}, description: ${JSON.stringify(r.description)} },`,
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
- const buffer = Buffer.from(blob, "base64");
26
- const decompressed = gunzipSync(buffer);
27
- const spec = JSON.parse(decompressed.toString("utf-8")) as OpenAPISpec;
28
- cache.set(domain, spec);
29
- return spec;
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
- const resource = url.searchParams.get("resource");
47
- const pathFilter = url.searchParams.get("path");
63
+ try {
64
+ const resource = url.searchParams.get("resource");
65
+ const pathFilter = url.searchParams.get("path");
48
66
 
49
- if (resource) {
50
- const spec = decompress(domain);
51
- const matchingPaths = filterPathsByResource(spec, resource);
52
- if (Object.keys(matchingPaths).length === 0) {
53
- return makeResource(url, renderUnknownResource(resource, entry, spec));
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, renderPathSpec(domain, pathFilter, spec));
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 groups = groupPathsBySchema(spec);
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 = (op as Record<string, unknown>).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 | Info |",
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, unknown>>> {
152
- const groups = new Map<string, Record<string, Record<string, unknown>>>();
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 = (op as Record<string, unknown>).operationId as string | undefined;
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 as Record<string, unknown>;
181
+ group[pathKey][method] = op;
167
182
  }
168
183
  }
169
184
 
170
185
  return groups;
171
186
  }
172
187
 
173
- function filterPathsByResource(spec: OpenAPISpec, resource: string): Record<string, Record<string, unknown>> {
174
- const groups = groupPathsBySchema(spec);
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, unknown>>>();
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, unknown>> = {};
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 as Record<string, unknown>;
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 as Array<Record<string, unknown>> | undefined;
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 as Record<string, unknown> | undefined;
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 as Record<string, Record<string, unknown>> | undefined;
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 as Record<string, unknown>;
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 = groupPathsBySchema(spec);
399
+ const groups = getCachedGroups(spec);
362
400
  const schemaNames = [...groups.keys()].sort();
363
401
 
364
402
  return [
@@ -8,6 +8,7 @@
8
8
  export interface ApiSpecDomainResource {
9
9
  readonly name: string;
10
10
  readonly description: string;
11
+ readonly schemaComponents?: readonly string[];
11
12
  }
12
13
 
13
14
  export interface ApiSpecDomainEntry {
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "18.30.0",
21
- "commit": "72667c39746cfce92ec26d5d81f2a99617e1d4fe",
22
- "shortCommit": "72667c3",
20
+ "version": "18.30.2",
21
+ "commit": "25941ca15e065dd3be59c1814a5ff8871b7af373",
22
+ "shortCommit": "25941ca",
23
23
  "branch": "main",
24
- "tag": "v18.30.0",
25
- "commitDate": "2026-05-01T03:26:38Z",
26
- "buildDate": "2026-05-01T03:52:47.886Z",
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/72667c39746cfce92ec26d5d81f2a99617e1d4fe",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.30.0"
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 Distributed Cloud API specifications ({{apiSpecDomainCount}} domains, v{{apiSpecVersion}}).
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 — always consult the spec first.
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
 
@@ -16,31 +16,22 @@ 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;
19
+ let _buildMeta: { version: string; repoSlug: string } | null = null;
20
20
 
21
- function getApiSpecMeta(): { domainCount: number; version: string } {
22
- if (_apiSpecMeta) return _apiSpecMeta;
21
+ function getBuildMeta(): { version: string; repoSlug: string } {
22
+ if (_buildMeta) return _buildMeta;
23
23
  try {
24
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",
25
+ const mod = require("./internal-urls/build-info.generated");
26
+ _buildMeta = {
27
+ version: mod.BUILD_INFO?.version ?? "unknown",
28
+ repoSlug: mod.BUILD_INFO?.repoSlug ?? "unknown",
29
29
  };
30
30
  } catch {
31
- _apiSpecMeta = { domainCount: 0, version: "unknown" };
31
+ _buildMeta = { version: "unknown", repoSlug: "unknown" };
32
32
  }
33
- return _apiSpecMeta;
33
+ return _buildMeta;
34
34
  }
35
-
36
- function apiSpecDomainCount(): number {
37
- return getApiSpecMeta().domainCount;
38
- }
39
-
40
- function apiSpecVersion(): string {
41
- return getApiSpecMeta().version;
42
- }
43
-
44
35
  interface AlwaysApplyRule {
45
36
  name: string;
46
37
  content: string;
@@ -301,7 +292,9 @@ async function getCachedGpu(): Promise<string | undefined> {
301
292
  async function getEnvironmentInfo(): Promise<Array<{ label: string; value: string }>> {
302
293
  const gpu = await getCachedGpu();
303
294
  const cpus = os.cpus();
295
+ const build = getBuildMeta();
304
296
  const entries: Array<{ label: string; value: string | undefined }> = [
297
+ { label: "xcsh", value: `v${build.version} (${build.repoSlug})` },
305
298
  { label: "OS", value: `${os.platform()} ${os.release()}` },
306
299
  { label: "Distro", value: os.type() },
307
300
  { label: "Kernel", value: os.version() },
@@ -658,8 +651,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
658
651
  secretsEnabled,
659
652
  context,
660
653
  knowledgeTopics: options.knowledgeTopics,
661
- apiSpecDomainCount: apiSpecDomainCount(),
662
- apiSpecVersion: apiSpecVersion(),
663
654
  };
664
655
  let rendered = prompt.render(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
665
656