@f5-sales-demo/xcsh 19.105.2 → 19.105.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/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ### Fixed
6
+
7
+ - Regenerated stale same-version release PRs when newer commits have merged into `main` ([#2727](https://github.com/f5-sales-demo/xcsh/issues/2727))
8
+
9
+ ## [19.105.3] - 2026-07-31
10
+
11
+ ### Fixed
12
+
13
+ - Removed Biome findings from generated Site CLI transport descriptions ([#2715](https://github.com/f5-sales-demo/xcsh/issues/2715))
14
+ - Restricted LiteLLM `models.yml` setup, auto-fix, discovery-upgrade, and backup writes to owner-only permissions ([#2713](https://github.com/f5-sales-demo/xcsh/issues/2713))
15
+ - Fixed `--no-tools` print requests forcing inactive `todo_write` through eager-todo enforcement ([#2711](https://github.com/f5-sales-demo/xcsh/issues/2711))
16
+
5
17
  ## [19.105.1] - 2026-07-31
6
18
 
7
19
  ### Fixed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/xcsh",
4
- "version": "19.105.2",
4
+ "version": "19.105.4",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Can Boluk",
@@ -57,13 +57,13 @@
57
57
  "dependencies": {
58
58
  "@agentclientprotocol/sdk": "1.3.0",
59
59
  "@mozilla/readability": "^0.6",
60
- "@f5-sales-demo/xcsh-stats": "19.105.2",
61
- "@f5-sales-demo/pi-agent-core": "19.105.2",
62
- "@f5-sales-demo/pi-ai": "19.105.2",
63
- "@f5-sales-demo/pi-natives": "19.105.2",
64
- "@f5-sales-demo/pi-resource-management": "19.105.2",
65
- "@f5-sales-demo/pi-tui": "19.105.2",
66
- "@f5-sales-demo/pi-utils": "19.105.2",
60
+ "@f5-sales-demo/xcsh-stats": "19.105.4",
61
+ "@f5-sales-demo/pi-agent-core": "19.105.4",
62
+ "@f5-sales-demo/pi-ai": "19.105.4",
63
+ "@f5-sales-demo/pi-natives": "19.105.4",
64
+ "@f5-sales-demo/pi-resource-management": "19.105.4",
65
+ "@f5-sales-demo/pi-tui": "19.105.4",
66
+ "@f5-sales-demo/pi-utils": "19.105.4",
67
67
  "@sinclair/typebox": "^0.34",
68
68
  "@xterm/headless": "^6.0",
69
69
  "ajv": "^8.20",
@@ -20,6 +20,8 @@ import { DEFAULT_MODEL_ROLE } from "./settings-schema";
20
20
 
21
21
  /** Current config schema version. Bump when the generated format changes. */
22
22
  export const CURRENT_CONFIG_VERSION = 2;
23
+ const LITELLM_CONFIG_DIR_MODE = 0o700;
24
+ const LITELLM_MODELS_FILE_MODE = 0o600;
23
25
 
24
26
  // ---------------------------------------------------------------------------
25
27
  // Detection
@@ -83,13 +85,13 @@ export function generateModelsYml(baseUrl: string, options?: GenerateModelsYmlOp
83
85
 
84
86
  /** Persist models.yml with owner-only permissions because it may contain a literal proxy credential. */
85
87
  export async function writeLiteLLMModelsYml(filePath: string, content: string): Promise<void> {
86
- await fs.promises.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
88
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true, mode: LITELLM_CONFIG_DIR_MODE });
87
89
  try {
88
- await fs.promises.chmod(filePath, 0o600);
90
+ await fs.promises.chmod(filePath, LITELLM_MODELS_FILE_MODE);
89
91
  } catch (error) {
90
92
  if (!isEnoent(error)) throw error;
91
93
  }
92
- await fs.promises.writeFile(filePath, content, { encoding: "utf-8", mode: 0o600 });
94
+ await fs.promises.writeFile(filePath, content, { encoding: "utf-8", mode: LITELLM_MODELS_FILE_MODE });
93
95
  }
94
96
 
95
97
  export interface LiteLLMConfig {
@@ -217,15 +219,13 @@ export function readApiKeyLiteral(modelsPath: string): string | undefined {
217
219
  return undefined;
218
220
  }
219
221
 
220
- /** Create a .bak backup of a file if it exists. Returns true if backed up. */
221
- function backupIfExists(filePath: string): boolean {
222
+ /** Create an owner-only .bak backup of models.yml. Returns true if backed up. */
223
+ function backupModelsConfigIfExists(filePath: string): boolean {
224
+ const backupPath = `${filePath}.bak`;
222
225
  try {
223
- if (fs.existsSync(filePath)) {
224
- fs.copyFileSync(filePath, `${filePath}.bak`);
225
- return true;
226
- }
226
+ return safeWriteModelsConfig(backupPath, fs.readFileSync(filePath));
227
227
  } catch (err) {
228
- logger.warn("Failed to create backup", { filePath, err });
228
+ if (!isEnoent(err)) logger.warn("Failed to create backup", { filePath, err });
229
229
  }
230
230
  return false;
231
231
  }
@@ -242,6 +242,23 @@ function safeWrite(filePath: string, content: string): boolean {
242
242
  }
243
243
  }
244
244
 
245
+ /** Write models.yml content without ever exposing it through group/world-readable permissions. */
246
+ function safeWriteModelsConfig(filePath: string, content: string | Uint8Array): boolean {
247
+ try {
248
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: LITELLM_CONFIG_DIR_MODE });
249
+ try {
250
+ fs.chmodSync(filePath, LITELLM_MODELS_FILE_MODE);
251
+ } catch (err) {
252
+ if (!isEnoent(err)) throw err;
253
+ }
254
+ fs.writeFileSync(filePath, content, { encoding: "utf-8", mode: LITELLM_MODELS_FILE_MODE });
255
+ return true;
256
+ } catch (err) {
257
+ logger.warn("Failed to write LiteLLM models config", { filePath, err });
258
+ return false;
259
+ }
260
+ }
261
+
245
262
  /** Remove the model cache database so discovery re-runs fresh. */
246
263
  function clearModelCache(modelsPath: string): void {
247
264
  const cacheDbPath = path.join(path.dirname(modelsPath), "models.db");
@@ -367,7 +384,7 @@ export function tryAutoConfigLiteLLM(modelsPath: string): boolean {
367
384
  const baseUrl = getLiteLLMBaseUrl();
368
385
  if (!baseUrl) return false;
369
386
 
370
- if (!safeWrite(modelsPath, generateModelsYml(baseUrl))) return false;
387
+ if (!safeWriteModelsConfig(modelsPath, generateModelsYml(baseUrl))) return false;
371
388
  logger.debug("Auto-configured LiteLLM proxy", { modelsPath, baseUrl });
372
389
 
373
390
  // Write config.yml if it doesn't exist, or heal it if it's missing modelRoles
@@ -501,10 +518,10 @@ export function autoFixModelsConfig(modelsPath: string): FixResult {
501
518
 
502
519
  const existingLiteralKey = readApiKeyLiteral(modelsPath);
503
520
 
504
- backupIfExists(modelsPath);
521
+ backupModelsConfigIfExists(modelsPath);
505
522
 
506
523
  if (
507
- !safeWrite(
524
+ !safeWriteModelsConfig(
508
525
  modelsPath,
509
526
  generateModelsYml(baseUrl, {
510
527
  ...(existingLiteralKey ? { apiKeyLiteral: existingLiteralKey } : {}),
@@ -688,12 +705,12 @@ export async function probeAndUpgradeLiteLLMConfig(
688
705
 
689
706
  // Upgrade: backup and regenerate with correct base path, preserving literal keys
690
707
  const existingLiteralKey = readApiKeyLiteral(modelsPath);
691
- backupIfExists(modelsPath);
708
+ backupModelsConfigIfExists(modelsPath);
692
709
  const newContent = generateModelsYml(baseUrl, {
693
710
  apiBasePath: probe.apiBasePath,
694
711
  ...(existingLiteralKey ? { apiKeyLiteral: existingLiteralKey } : {}),
695
712
  });
696
- if (!safeWrite(modelsPath, newContent)) {
713
+ if (!safeWriteModelsConfig(modelsPath, newContent)) {
697
714
  return false;
698
715
  }
699
716
 
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "19.105.2",
21
- "commit": "3393e245e239a6aa875c405931a8814b1acb26a4",
22
- "shortCommit": "3393e24",
20
+ "version": "19.105.4",
21
+ "commit": "c2e7e7b8b1853d1bde35b70e8f34765e895fa54e",
22
+ "shortCommit": "c2e7e7b",
23
23
  "branch": "main",
24
- "tag": "v19.105.2",
25
- "commitDate": "2026-07-31T16:38:06Z",
26
- "buildDate": "2026-07-31T17:06:38.688Z",
24
+ "tag": "v19.105.4",
25
+ "commitDate": "2026-07-31T17:41:55Z",
26
+ "buildDate": "2026-07-31T18:14:15.917Z",
27
27
  "dirty": true,
28
28
  "prNumber": "",
29
29
  "repoUrl": "https://github.com/f5-sales-demo/xcsh",
30
30
  "repoSlug": "f5-sales-demo/xcsh",
31
- "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/3393e245e239a6aa875c405931a8814b1acb26a4",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.105.2"
31
+ "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/c2e7e7b8b1853d1bde35b70e8f34765e895fa54e",
32
+ "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.105.4"
33
33
  };
@@ -668,10 +668,10 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
668
668
  ...(entry.example ? [`- example argument: \`${entry.example}\``] : []),
669
669
  "",
670
670
  entry.transport === "global-get"
671
- ? "GLOBAL scope: GET .../vpm/debug/global/" + subpath + " — NOT reachable via exec-user."
671
+ ? `GLOBAL scope: GET .../vpm/debug/global/${subpath} — NOT reachable via exec-user.`
672
672
  : entry.transport === "exec"
673
673
  ? "Exec tier: privileged and mutating. Do not run speculatively."
674
- : 'POST .../vpm/debug/{node}/exec-user with {"command":["' + subpath + '", ...]}.',
674
+ : `POST .../vpm/debug/{node}/exec-user with {"command":["${subpath}", ...]}.`,
675
675
  ].join("\n")
676
676
  : `Unknown Site CLI command: ${subpath}\n\nRead xcsh://sitecli for the full list (build ${build}).`;
677
677
  }
@@ -4391,7 +4391,7 @@ export class AgentSession {
4391
4391
  return undefined;
4392
4392
  }
4393
4393
 
4394
- if (!this.#toolRegistry.has("todo_write")) {
4394
+ if (!this.agent.state.tools.some(tool => tool.name === "todo_write")) {
4395
4395
  logger.warn("Eager todo enforcement skipped because todo_write is unavailable", {
4396
4396
  activeToolNames: this.agent.state.tools.map(tool => tool.name),
4397
4397
  });