@f5xc-salesdemos/xcsh 17.1.2 → 17.1.4

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": "17.1.2",
4
+ "version": "17.1.4",
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": "17.1.2",
50
- "@f5xc-salesdemos/pi-agent-core": "17.1.2",
51
- "@f5xc-salesdemos/pi-ai": "17.1.2",
52
- "@f5xc-salesdemos/pi-natives": "17.1.2",
53
- "@f5xc-salesdemos/pi-tui": "17.1.2",
54
- "@f5xc-salesdemos/pi-utils": "17.1.2",
49
+ "@f5xc-salesdemos/xcsh-stats": "17.1.4",
50
+ "@f5xc-salesdemos/pi-agent-core": "17.1.4",
51
+ "@f5xc-salesdemos/pi-ai": "17.1.4",
52
+ "@f5xc-salesdemos/pi-natives": "17.1.4",
53
+ "@f5xc-salesdemos/pi-tui": "17.1.4",
54
+ "@f5xc-salesdemos/pi-utils": "17.1.4",
55
55
  "@sinclair/typebox": "^0.34",
56
56
  "@xterm/headless": "^6.0",
57
57
  "ajv": "^8.18",
@@ -199,6 +199,35 @@ export function healConfigYmlModelRoles(configPath: string): void {
199
199
  // Backup
200
200
  // ---------------------------------------------------------------------------
201
201
 
202
+ /**
203
+ * Extract a quoted literal API key from an existing models.yml file.
204
+ * Only looks within the anthropic or litellm provider blocks to avoid
205
+ * accidentally picking up keys from unrelated providers.
206
+ * Returns undefined if the file uses an env var reference (unquoted) or doesn't exist.
207
+ */
208
+ export function readApiKeyLiteral(modelsPath: string): string | undefined {
209
+ try {
210
+ const content = fs.readFileSync(modelsPath, "utf-8");
211
+ const lines = content.split("\n");
212
+ let inTargetBlock = false;
213
+
214
+ for (const line of lines) {
215
+ if (/^\s{2}(?:anthropic|litellm)\s*:/.test(line)) {
216
+ inTargetBlock = true;
217
+ continue;
218
+ }
219
+ if (inTargetBlock && /^\s{2}\S/.test(line)) {
220
+ inTargetBlock = false;
221
+ }
222
+ if (inTargetBlock) {
223
+ const match = line.match(/^\s+apiKey:\s*"([^"]+)"/);
224
+ if (match) return match[1].replace(/\\"/g, '"').replace(/\\\\/g, "\\");
225
+ }
226
+ }
227
+ } catch {}
228
+ return undefined;
229
+ }
230
+
202
231
  /** Create a .bak backup of a file if it exists. Returns true if backed up. */
203
232
  function backupIfExists(filePath: string): boolean {
204
233
  try {
@@ -481,9 +510,18 @@ export function autoFixModelsConfig(modelsPath: string): FixResult {
481
510
  return { fixed: false, changes: ["Cannot fix: LITELLM_BASE_URL is invalid"] };
482
511
  }
483
512
 
513
+ const existingLiteralKey = readApiKeyLiteral(modelsPath);
514
+
484
515
  backupIfExists(modelsPath);
485
516
 
486
- if (!safeWrite(modelsPath, generateModelsYml(baseUrl))) {
517
+ if (
518
+ !safeWrite(
519
+ modelsPath,
520
+ generateModelsYml(baseUrl, {
521
+ ...(existingLiteralKey ? { apiKeyLiteral: existingLiteralKey } : {}),
522
+ }),
523
+ )
524
+ ) {
487
525
  return { fixed: false, changes: [`Write failed: could not write to ${modelsPath}`] };
488
526
  }
489
527
 
@@ -538,13 +576,13 @@ export function startupHealthCheck(
538
576
 
539
577
  const expectedUrl = `${envBaseUrl}/anthropic`;
540
578
  if (anthropicConfig.baseUrl !== expectedUrl) {
541
- // Only auto-fix configs that were generated by xcsh (they contain the
542
- // literal string "apiKey: LITELLM_API_KEY"). User-written configs with a
543
- // custom proxy URL must never be silently overwritten.
579
+ // Only auto-fix configs that were generated by xcsh. User-written configs
580
+ // with a custom proxy URL must never be silently overwritten.
581
+ // Recognize both env-var-ref configs and literal-key configs as auto-generated.
544
582
  let isAutoGenerated = false;
545
583
  try {
546
584
  const content = fs.readFileSync(modelsPath, "utf-8");
547
- isAutoGenerated = content.includes("apiKey: LITELLM_API_KEY");
585
+ isAutoGenerated = content.includes("Auto-generated by xcsh") || content.includes("apiKey: LITELLM_API_KEY");
548
586
  } catch {
549
587
  // File unreadable — skip, don't block startup
550
588
  }
@@ -608,10 +646,18 @@ export async function probeAndUpgradeLiteLLMConfig(
608
646
  modelsPath: string,
609
647
  options?: { fetch?: typeof globalThis.fetch },
610
648
  ): Promise<boolean> {
611
- if (!hasLiteLLMEnv()) return false;
649
+ // Try env vars first; fall back to literal key stored in the config
650
+ let baseUrl = getLiteLLMBaseUrl();
651
+ let apiKey = $env.LITELLM_API_KEY?.trim();
652
+
653
+ if (!baseUrl || !apiKey) {
654
+ const existing = readLiteLLMConfig(modelsPath);
655
+ if (existing) {
656
+ baseUrl = baseUrl || existing.baseUrl;
657
+ apiKey = apiKey || existing.apiKey;
658
+ }
659
+ }
612
660
 
613
- const baseUrl = getLiteLLMBaseUrl();
614
- const apiKey = $env.LITELLM_API_KEY?.trim();
615
661
  if (!baseUrl || !apiKey) return false;
616
662
 
617
663
  let content: string;
@@ -624,7 +670,8 @@ export async function probeAndUpgradeLiteLLMConfig(
624
670
 
625
671
  // Only upgrade configs that were auto-generated by xcsh. User-written configs
626
672
  // with custom proxy URLs must never be silently overwritten by LiteLLM probing.
627
- if (!content.includes("apiKey: LITELLM_API_KEY")) {
673
+ // Recognize both env-var-ref configs and literal-key configs as auto-generated.
674
+ if (!content.includes("Auto-generated by xcsh") && !content.includes("apiKey: LITELLM_API_KEY")) {
628
675
  return false;
629
676
  }
630
677
 
@@ -650,9 +697,13 @@ export async function probeAndUpgradeLiteLLMConfig(
650
697
  return false; // Already correct
651
698
  }
652
699
 
653
- // Upgrade: backup and regenerate with correct base path
700
+ // Upgrade: backup and regenerate with correct base path, preserving literal keys
701
+ const existingLiteralKey = readApiKeyLiteral(modelsPath);
654
702
  backupIfExists(modelsPath);
655
- const newContent = generateModelsYml(baseUrl, { apiBasePath: probe.apiBasePath });
703
+ const newContent = generateModelsYml(baseUrl, {
704
+ apiBasePath: probe.apiBasePath,
705
+ ...(existingLiteralKey ? { apiKeyLiteral: existingLiteralKey } : {}),
706
+ });
656
707
  if (!safeWrite(modelsPath, newContent)) {
657
708
  return false;
658
709
  }
@@ -461,12 +461,26 @@ type LlamaCppDiscoveredServerMetadata = {
461
461
  * Resolve an API key config value to an actual key.
462
462
  * Checks environment variable first, then treats as literal.
463
463
  */
464
- function resolveApiKeyConfig(keyConfig: string): string | undefined {
464
+ export function resolveApiKeyConfig(keyConfig: string): string | undefined {
465
465
  const envValue = Bun.env[keyConfig];
466
466
  if (envValue) return envValue;
467
467
  return keyConfig;
468
468
  }
469
469
 
470
+ const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z][A-Z0-9]*)+$/;
471
+
472
+ /**
473
+ * Resolve an API key that came from YAML config (models.yml).
474
+ * Unlike resolveApiKeyConfig, this returns undefined for unresolved env var
475
+ * names to prevent sending literal names like "LITELLM_API_KEY" as Bearer tokens.
476
+ */
477
+ export function resolveYamlApiKeyConfig(keyConfig: string): string | undefined {
478
+ const envValue = Bun.env[keyConfig];
479
+ if (envValue) return envValue;
480
+ if (ENV_VAR_NAME_RE.test(keyConfig)) return undefined;
481
+ return keyConfig;
482
+ }
483
+
470
484
  function toPositiveNumberOrUndefined(value: unknown): number | undefined {
471
485
  if (typeof value === "number" && Number.isFinite(value) && value > 0) {
472
486
  return value;
@@ -67,6 +67,11 @@ async function validateModelConnection(model: Model | undefined, authStorage: Au
67
67
  return { state: "auth_error", provider };
68
68
  }
69
69
 
70
+ // Detect unresolved env var names (e.g. "LITELLM_API_KEY" sent as literal)
71
+ if (/^[A-Z][A-Z0-9]*(?:_[A-Z][A-Z0-9]*)+$/.test(rawApiKey)) {
72
+ return { state: "auth_error", provider };
73
+ }
74
+
70
75
  const baseUrl = model?.baseUrl;
71
76
  if (!baseUrl) {
72
77
  return { state: "auth_error", provider };