@f5xc-salesdemos/xcsh 18.66.0 → 18.66.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.66.0",
4
+ "version": "18.66.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.66.0",
52
- "@f5xc-salesdemos/pi-agent-core": "18.66.0",
53
- "@f5xc-salesdemos/pi-ai": "18.66.0",
54
- "@f5xc-salesdemos/pi-natives": "18.66.0",
55
- "@f5xc-salesdemos/pi-tui": "18.66.0",
56
- "@f5xc-salesdemos/pi-utils": "18.66.0",
51
+ "@f5xc-salesdemos/xcsh-stats": "18.66.1",
52
+ "@f5xc-salesdemos/pi-agent-core": "18.66.1",
53
+ "@f5xc-salesdemos/pi-ai": "18.66.1",
54
+ "@f5xc-salesdemos/pi-natives": "18.66.1",
55
+ "@f5xc-salesdemos/pi-tui": "18.66.1",
56
+ "@f5xc-salesdemos/pi-utils": "18.66.1",
57
57
  "@sinclair/typebox": "^0.34",
58
58
  "@xterm/headless": "^6.0",
59
59
  "ajv": "^8.18",
@@ -90,6 +90,35 @@ const REPO = "f5xc-salesdemos/api-specs-enriched";
90
90
  const outputPath = path.resolve(import.meta.dir, "../src/internal-urls/api-spec-index.generated.ts");
91
91
  const catalogOutputPath = path.resolve(import.meta.dir, "../src/internal-urls/api-catalog-index.generated.ts");
92
92
 
93
+ const MAX_RETRIES = 3;
94
+ const INITIAL_BACKOFF_MS = 2000;
95
+
96
+ async function fetchWithRetry(url: string, init?: RequestInit): Promise<Response> {
97
+ let lastError: Error | null = null;
98
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
99
+ if (attempt > 0) {
100
+ const delay = INITIAL_BACKOFF_MS * 2 ** (attempt - 1);
101
+ console.warn(` Retry ${attempt}/${MAX_RETRIES} after ${delay}ms...`);
102
+ await Bun.sleep(delay);
103
+ }
104
+ try {
105
+ const response = await fetch(url, init);
106
+ if (response.status === 403 || response.status === 429) {
107
+ const retryAfter = response.headers.get("retry-after");
108
+ const waitMs = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : INITIAL_BACKOFF_MS * 2 ** attempt;
109
+ console.warn(` Rate limited (${response.status}), waiting ${waitMs}ms...`);
110
+ await Bun.sleep(waitMs);
111
+ continue;
112
+ }
113
+ return response;
114
+ } catch (err) {
115
+ lastError = err instanceof Error ? err : new Error(String(err));
116
+ console.warn(` Fetch failed: ${lastError.message}`);
117
+ }
118
+ }
119
+ throw lastError ?? new Error(`Failed to fetch ${url} after ${MAX_RETRIES} retries`);
120
+ }
121
+
93
122
  function githubHeaders(): Record<string, string> {
94
123
  const headers: Record<string, string> = { Accept: "application/vnd.github+json" };
95
124
  const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
@@ -98,7 +127,7 @@ function githubHeaders(): Record<string, string> {
98
127
  }
99
128
 
100
129
  async function resolveLatestTag(): Promise<string> {
101
- const response = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, {
130
+ const response = await fetchWithRetry(`https://api.github.com/repos/${REPO}/releases/latest`, {
102
131
  headers: githubHeaders(),
103
132
  });
104
133
  if (!response.ok) {
@@ -119,7 +148,7 @@ async function downloadFromRelease(): Promise<string> {
119
148
  const downloadUrl = `https://github.com/${REPO}/releases/download/${tag}/${zipName}`;
120
149
 
121
150
  console.log(`Downloading API specs from ${downloadUrl}...`);
122
- const response = await fetch(downloadUrl, { redirect: "follow" });
151
+ const response = await fetchWithRetry(downloadUrl, { redirect: "follow" });
123
152
  if (!response.ok) {
124
153
  throw new Error(`Failed to download release: ${response.status} ${response.statusText}`);
125
154
  }
@@ -172,7 +201,7 @@ async function downloadCatalog(specsDir: string): Promise<Record<string, unknown
172
201
  const catalogUrl = `https://github.com/${REPO}/releases/download/${catalogTag}/api-catalog.json`;
173
202
  console.log(`Downloading API catalog from ${catalogUrl}...`);
174
203
  try {
175
- const response = await fetch(catalogUrl, { redirect: "follow" });
204
+ const response = await fetchWithRetry(catalogUrl, { redirect: "follow" });
176
205
  if (!response.ok) {
177
206
  console.warn(`api-catalog.json not found (${response.status}), skipping catalog generation`);
178
207
  return null;
@@ -192,6 +221,11 @@ function serializeEnrichment(key: string, value: unknown): string | undefined {
192
221
 
193
222
  let downloadedTmpDir: string | null = null;
194
223
 
224
+ if (fs.existsSync(outputPath) && fs.existsSync(catalogOutputPath) && process.env.CI) {
225
+ console.log("Generated spec files already exist in CI — skipping regeneration.");
226
+ process.exit(0);
227
+ }
228
+
195
229
  const specsDir = await findSpecsDir();
196
230
  console.log(`Reading specs from: ${specsDir}`);
197
231
 
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "18.66.0",
21
- "commit": "e018080e23d781f41f31f5fddb031abf40358670",
22
- "shortCommit": "e018080",
20
+ "version": "18.66.1",
21
+ "commit": "90d34ed7bee228bbc7e09126b5e9e71aee873861",
22
+ "shortCommit": "90d34ed",
23
23
  "branch": "main",
24
- "tag": "v18.66.0",
25
- "commitDate": "2026-05-17T18:46:03Z",
26
- "buildDate": "2026-05-17T19:10:23.269Z",
24
+ "tag": "v18.66.1",
25
+ "commitDate": "2026-05-18T06:44:43Z",
26
+ "buildDate": "2026-05-18T07:08:42.105Z",
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/e018080e23d781f41f31f5fddb031abf40358670",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.66.0"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/90d34ed7bee228bbc7e09126b5e9e71aee873861",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.66.1"
33
33
  };
@@ -4,7 +4,7 @@ import { Text } from "@f5xc-salesdemos/pi-tui";
4
4
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
5
5
  import type { Theme, ThemeColor } from "../modes/theme/theme";
6
6
  import { highlightCode } from "../modes/theme/theme";
7
- import { CachedOutputBlock, renderStatusLine } from "../tui";
7
+ import { CachedOutputBlock, F5_TOOL_BORDER_COLOR, renderStatusLine } from "../tui";
8
8
  import { formatErrorMessage, replaceTabs } from "./render-utils";
9
9
  import type { XcshApiToolDetails } from "./xcsh-api";
10
10
 
@@ -209,7 +209,7 @@ export const xcshApiToolRenderer = {
209
209
  render(width: number): string[] {
210
210
  const state = options.isPartial ? "pending" : "success";
211
211
  return batchBlock.render(
212
- { header: batchHeader, state, sections: batchSections, width, borderColor: "border" },
212
+ { header: batchHeader, state, sections: batchSections, width, borderColor: F5_TOOL_BORDER_COLOR },
213
213
  uiTheme,
214
214
  );
215
215
  },
@@ -383,7 +383,10 @@ export const xcshApiToolRenderer = {
383
383
  return {
384
384
  render(width: number): string[] {
385
385
  const state = options.isPartial ? "pending" : isError ? "error" : "success";
386
- return outputBlock.render({ header, state, sections, width, borderColor: "border" }, uiTheme);
386
+ return outputBlock.render(
387
+ { header, state, sections, width, borderColor: isError ? undefined : F5_TOOL_BORDER_COLOR },
388
+ uiTheme,
389
+ );
387
390
  },
388
391
  invalidate() {
389
392
  outputBlock.invalidate();
@@ -8,6 +8,12 @@ import type { State } from "./types";
8
8
  import type { RenderCache } from "./utils";
9
9
  import { getStateBgColor, Hasher, padToWidth, truncateToWidth } from "./utils";
10
10
 
11
+ /**
12
+ * Border color override for F5-branded tool renderers.
13
+ * Pass as `borderColor` in OutputBlockOptions to apply F5 red framing regardless of tool state.
14
+ */
15
+ export const F5_TOOL_BORDER_COLOR: ThemeColor = "border";
16
+
11
17
  export interface OutputBlockOptions {
12
18
  header?: string;
13
19
  headerMeta?: string;
@@ -33,17 +39,12 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
33
39
  const v = theme.boxSharp.vertical;
34
40
  const cap = h.repeat(3);
35
41
  const lineWidth = Math.max(0, width);
36
- // Border colors: running/pending use accent, success uses dim (gray), error/warning keep their colors.
37
- // borderColorOverride (from options) takes precedence for branded core tools (e.g. XC-API).
42
+ // Border colors: running/pending use accent, everything else uses dim (gray).
43
+ // Error state uses dim the red toolErrorBg background is the error signal; no red border on generic tools.
44
+ // borderColorOverride (from options) takes precedence for F5-branded tools (e.g. XC-API); use F5_TOOL_BORDER_COLOR.
38
45
  const resolvedBorderColor: ThemeColor =
39
46
  borderColorOverride ??
40
- (state === "error"
41
- ? "error"
42
- : state === "warning"
43
- ? "warning"
44
- : state === "running" || state === "pending"
45
- ? "spinnerAccent"
46
- : "dim");
47
+ (state === "warning" ? "warning" : state === "running" || state === "pending" ? "spinnerAccent" : "dim");
47
48
  const border = (text: string) => theme.fg(resolvedBorderColor, text);
48
49
  const bgFn = (() => {
49
50
  if (!state || !applyBg) return undefined;