@f5xc-salesdemos/xcsh 15.9.0 → 15.9.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": "15.9.0",
4
+ "version": "15.9.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",
@@ -46,12 +46,12 @@
46
46
  "dependencies": {
47
47
  "@agentclientprotocol/sdk": "0.16.1",
48
48
  "@mozilla/readability": "^0.6",
49
- "@f5xc-salesdemos/xcsh-stats": "15.9.0",
50
- "@f5xc-salesdemos/pi-agent-core": "15.9.0",
51
- "@f5xc-salesdemos/pi-ai": "15.9.0",
52
- "@f5xc-salesdemos/pi-natives": "15.9.0",
53
- "@f5xc-salesdemos/pi-tui": "15.9.0",
54
- "@f5xc-salesdemos/pi-utils": "15.9.0",
49
+ "@f5xc-salesdemos/xcsh-stats": "15.9.2",
50
+ "@f5xc-salesdemos/pi-agent-core": "15.9.2",
51
+ "@f5xc-salesdemos/pi-ai": "15.9.2",
52
+ "@f5xc-salesdemos/pi-natives": "15.9.2",
53
+ "@f5xc-salesdemos/pi-tui": "15.9.2",
54
+ "@f5xc-salesdemos/pi-utils": "15.9.2",
55
55
  "@sinclair/typebox": "^0.34",
56
56
  "@xterm/headless": "^6.0",
57
57
  "ajv": "^8.18",
@@ -458,6 +458,18 @@ export function startupHealthCheck(
458
458
 
459
459
  const expectedUrl = `${envBaseUrl}/anthropic`;
460
460
  if (anthropicConfig.baseUrl !== expectedUrl) {
461
+ // Only auto-fix configs that were generated by xcsh (they contain the
462
+ // literal string "apiKey: LITELLM_API_KEY"). User-written configs with a
463
+ // custom proxy URL must never be silently overwritten.
464
+ let isAutoGenerated = false;
465
+ try {
466
+ const content = fs.readFileSync(modelsPath, "utf-8");
467
+ isAutoGenerated = content.includes("apiKey: LITELLM_API_KEY");
468
+ } catch {
469
+ // File unreadable — skip, don't block startup
470
+ }
471
+ if (!isAutoGenerated) return false;
472
+
461
473
  logger.warn("LiteLLM config drift detected — auto-fixing", {
462
474
  configured: anthropicConfig.baseUrl,
463
475
  expected: expectedUrl,
@@ -530,6 +542,12 @@ export async function probeAndUpgradeLiteLLMConfig(
530
542
  return false;
531
543
  }
532
544
 
545
+ // Only upgrade configs that were auto-generated by xcsh. User-written configs
546
+ // with custom proxy URLs must never be silently overwritten by LiteLLM probing.
547
+ if (!content.includes("apiKey: LITELLM_API_KEY")) {
548
+ return false;
549
+ }
550
+
533
551
  // Probe the proxy to find the working API base path
534
552
  const probe = await probeLiteLLMConnection(baseUrl, apiKey, { fetch: options?.fetch });
535
553
  if (!probe.reachable) {
@@ -25,7 +25,7 @@ import {
25
25
  unregisterCustomApis,
26
26
  unregisterOAuthProviders,
27
27
  } from "@f5xc-salesdemos/pi-ai";
28
- import { isRecord, logger } from "@f5xc-salesdemos/pi-utils";
28
+ import { $env, isRecord, logger } from "@f5xc-salesdemos/pi-utils";
29
29
  import { type Static, Type } from "@sinclair/typebox";
30
30
  import { type ConfigError, ConfigFile } from "../config";
31
31
  import { hasLiteLLMEnv, probeAndUpgradeLiteLLMConfig, startupHealthCheck } from "../config/auto-config";
@@ -1307,12 +1307,20 @@ export class ModelRegistry {
1307
1307
  // Skip providers already handled by configured discovery (e.g. user-configured ollama with discovery.type)
1308
1308
  const configuredDiscoveryProviders = new Set(this.#discoverableProviders.map(p => p.provider));
1309
1309
 
1310
- // When a LiteLLM proxy is configured, providers with overridden baseUrls are
1311
- // proxied through it. Their built-in discovery would query the proxy's model
1312
- // listing endpoint, which may return model IDs the proxy can't serve for chat.
1313
- // Skip them — the litellm discovery provider handles model listing instead.
1314
- const proxiedProviders = hasLiteLLMEnv()
1315
- ? new Set([...this.#providerOverrides.keys()].filter(id => this.#providerOverrides.get(id)?.baseUrl))
1310
+ // When a LiteLLM proxy is configured, providers whose baseUrl points to the
1311
+ // LiteLLM proxy are proxied through it. Their built-in discovery would query
1312
+ // the proxy's model listing endpoint, which may return model IDs the proxy
1313
+ // can't serve for chat. Skip them — the litellm discovery provider handles
1314
+ // model listing instead. Providers with other custom baseUrls (not LiteLLM)
1315
+ // still need built-in discovery to discover new models at their endpoint.
1316
+ const liteLLMBaseUrl = hasLiteLLMEnv() ? $env.LITELLM_BASE_URL?.trim().replace(/\/+$/, "") : undefined;
1317
+ const proxiedProviders = liteLLMBaseUrl
1318
+ ? new Set(
1319
+ [...this.#providerOverrides.keys()].filter(id => {
1320
+ const override = this.#providerOverrides.get(id);
1321
+ return override?.baseUrl?.startsWith(liteLLMBaseUrl);
1322
+ }),
1323
+ )
1316
1324
  : new Set<string>();
1317
1325
 
1318
1326
  const managerOptions = (await this.#collectBuiltInModelManagerOptions()).filter(opts => {
@@ -44,18 +44,23 @@ export interface TableOptions {
44
44
  dividerBefore?: number; // insert ├──┤ divider before this row index
45
45
  }
46
46
 
47
+ // Measures the visible terminal column width of a string.
48
+ // Delegates to Bun.stringWidth() which strips ANSI escape sequences and handles
49
+ // Unicode wide characters — the same underlying function used by @f5xc-salesdemos/pi-tui.
50
+ const visibleWidth = (s: string): number => (s ? Bun.stringWidth(s) : 0);
51
+
47
52
  export function renderF5XCTable(title: string, rows: TableRow[], options?: TableOptions): string {
48
- // Calculate column widths from visible text (strip ANSI for width calc)
49
- const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, "");
50
- const maxKey = Math.max(...rows.map(row => stripAnsi(row.key).length), 0);
51
- const maxVal = Math.max(...rows.map(row => stripAnsi(row.value).length), 0);
52
- const innerWidth = Math.max(maxKey + maxVal + 3, stripAnsi(title).length + 2, 40);
53
+ // Calculate column widths using visibleWidth (handles ANSI and Unicode)
54
+ const maxKey = Math.max(...rows.map(row => visibleWidth(row.key)), 0);
55
+ const maxVal = Math.max(...rows.map(row => visibleWidth(row.value)), 0);
56
+ // innerWidth = space + maxKey + 2-space separator + maxVal + space = maxKey + maxVal + 4
57
+ const innerWidth = Math.max(maxKey + maxVal + 4, visibleWidth(title) + 2, 40);
53
58
 
54
59
  const lines: string[] = [];
55
60
 
56
61
  // Top border: ╭─ title ──────╮
57
62
  const titleText = ` ${title} `;
58
- const titlePad = innerWidth - stripAnsi(titleText).length - 1;
63
+ const titlePad = innerWidth - visibleWidth(titleText) - 1;
59
64
  lines.push(`${r(BOX.tl + BOX.h)}${BOLD}${titleText}${RESET}${r(BOX.h.repeat(Math.max(0, titlePad)) + BOX.tr)}`);
60
65
 
61
66
  // Rows
@@ -63,13 +68,13 @@ export function renderF5XCTable(title: string, rows: TableRow[], options?: Table
63
68
  // Optional divider
64
69
  if (options?.dividerBefore === i) {
65
70
  const divLabel = " Environment ";
66
- const divPad = innerWidth - stripAnsi(divLabel).length - 1;
71
+ const divPad = innerWidth - visibleWidth(divLabel) - 1;
67
72
  lines.push(`${r(BOX.lt + BOX.h)}${BOLD}${divLabel}${RESET}${r(BOX.h.repeat(Math.max(0, divPad)) + BOX.rt)}`);
68
73
  }
69
74
 
70
75
  const { key, value } = rows[i];
71
- const keyPad = maxKey - stripAnsi(key).length;
72
- const valPad = innerWidth - maxKey - stripAnsi(value).length - 3;
76
+ const keyPad = maxKey - visibleWidth(key);
77
+ const valPad = innerWidth - maxKey - visibleWidth(value) - 4;
73
78
  lines.push(`${r(BOX.v)} ${key}${" ".repeat(keyPad)} ${value}${" ".repeat(Math.max(0, valPad))} ${r(BOX.v)}`);
74
79
  }
75
80