@f5-sales-demo/xcsh 20.14.0 → 20.15.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": "@f5-sales-demo/xcsh",
4
- "version": "20.14.0",
4
+ "version": "20.15.2",
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",
@@ -61,13 +61,13 @@
61
61
  },
62
62
  "dependencies": {
63
63
  "@agentclientprotocol/sdk": "1.3.0",
64
- "@f5-sales-demo/pi-agent-core": "20.14.0",
65
- "@f5-sales-demo/pi-ai": "20.14.0",
66
- "@f5-sales-demo/pi-natives": "20.14.0",
67
- "@f5-sales-demo/pi-resource-management": "20.14.0",
68
- "@f5-sales-demo/pi-tui": "20.14.0",
69
- "@f5-sales-demo/pi-utils": "20.14.0",
70
- "@f5-sales-demo/xcsh-stats": "20.14.0",
64
+ "@f5-sales-demo/pi-agent-core": "20.15.2",
65
+ "@f5-sales-demo/pi-ai": "20.15.2",
66
+ "@f5-sales-demo/pi-natives": "20.15.2",
67
+ "@f5-sales-demo/pi-resource-management": "20.15.2",
68
+ "@f5-sales-demo/pi-tui": "20.15.2",
69
+ "@f5-sales-demo/pi-utils": "20.15.2",
70
+ "@f5-sales-demo/xcsh-stats": "20.15.2",
71
71
  "@mozilla/readability": "^0.6",
72
72
  "@sinclair/typebox": "^0.34",
73
73
  "@xterm/headless": "^6.0",
@@ -2,13 +2,12 @@ import type { ThinkingLevel } from "@f5-sales-demo/pi-agent-core";
2
2
  import type { Api, Model } from "@f5-sales-demo/pi-ai";
3
3
  import { MODEL_ROLE_IDS } from "../config/model-registry";
4
4
  import {
5
+ findPriorityRoleModel,
5
6
  type ModelLookupRegistry,
6
- parseModelPattern,
7
7
  resolveModelRoleValue,
8
8
  resolveRoleSelection,
9
9
  } from "../config/model-resolver";
10
10
  import type { Settings } from "../config/settings";
11
- import MODEL_PRIO from "../priority.json" with { type: "json" };
12
11
 
13
12
  export interface ResolvedCommitModel {
14
13
  model: Model<Api>;
@@ -54,10 +53,8 @@ export async function resolveSmolModel(
54
53
  if (apiKey) return { model: resolvedSmol.model, apiKey, thinkingLevel: resolvedSmol.thinkingLevel };
55
54
  }
56
55
 
57
- const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
58
- for (const pattern of MODEL_PRIO.smol) {
59
- const candidate = parseModelPattern(pattern, available, matchPreferences, { modelRegistry }).model;
60
- if (!candidate) continue;
56
+ const candidate = await findPriorityRoleModel(modelRegistry, "smol", undefined, false);
57
+ if (candidate) {
61
58
  const apiKey = await modelRegistry.getApiKey(candidate);
62
59
  if (apiKey) return { model: candidate, apiKey };
63
60
  }
@@ -1470,10 +1470,27 @@ export class ModelRegistry {
1470
1470
  try {
1471
1471
  const manager = createModelManager({ ...options, cacheDbPath: this.#cacheDbPath });
1472
1472
  const result = await manager.refresh(strategy);
1473
- return result.models.map(model =>
1473
+ const models = result.models.map(model =>
1474
1474
  model.provider === options.providerId ? model : { ...model, provider: options.providerId },
1475
1475
  );
1476
+ this.#providerDiscoveryStates.set(options.providerId, {
1477
+ provider: options.providerId,
1478
+ status: result.stale ? "cached" : "ok",
1479
+ optional: false,
1480
+ stale: result.stale,
1481
+ fetchedAt: Date.now(),
1482
+ models: models.map(model => model.id),
1483
+ });
1484
+ return models;
1476
1485
  } catch (error) {
1486
+ this.#providerDiscoveryStates.set(options.providerId, {
1487
+ provider: options.providerId,
1488
+ status: "unavailable",
1489
+ optional: false,
1490
+ stale: true,
1491
+ models: [],
1492
+ error: error instanceof Error ? error.message : String(error),
1493
+ });
1477
1494
  logger.warn("model discovery failed for provider", {
1478
1495
  provider: options.providerId,
1479
1496
  error: error instanceof Error ? error.message : String(error),
@@ -1225,32 +1225,60 @@ export async function findSmolModel(
1225
1225
  modelRegistry: ModelLookupRegistry,
1226
1226
  savedModel?: string,
1227
1227
  ): Promise<Model<Api> | undefined> {
1228
+ return findPriorityRoleModel(modelRegistry, "smol", savedModel);
1229
+ }
1230
+
1231
+ export async function findPriorityRoleModel(
1232
+ modelRegistry: ModelLookupRegistry,
1233
+ role: "smol" | "slow",
1234
+ savedModel?: string,
1235
+ fallbackToFirst = true,
1236
+ ): Promise<Model<Api> | undefined> {
1237
+ return resolvePriorityRoleCandidates(modelRegistry, role, savedModel, fallbackToFirst)[0];
1238
+ }
1239
+
1240
+ /** Shared ordered candidates for role consumers that need fallback retries. */
1241
+ export function resolvePriorityRoleCandidates(
1242
+ modelRegistry: ModelLookupRegistry,
1243
+ role: "smol" | "slow",
1244
+ savedModel?: string,
1245
+ includeAllFallbacks = true,
1246
+ ): Model<Api>[] {
1228
1247
  const availableModels = modelRegistry.getAvailable();
1229
- if (availableModels.length === 0) return undefined;
1248
+ if (availableModels.length === 0) return [];
1249
+ const candidates: Model<Api>[] = [];
1250
+ const add = (model: Model<Api> | undefined): void => {
1251
+ if (!model) return;
1252
+ if (candidates.some(item => item.provider === model.provider && item.id === model.id)) return;
1253
+ candidates.push(model);
1254
+ };
1230
1255
 
1231
1256
  // 1. Try saved model from settings
1232
1257
  if (savedModel) {
1233
1258
  const match = resolveModelFromString(savedModel, availableModels, undefined, modelRegistry);
1234
- if (match) return match;
1259
+ add(match);
1235
1260
  }
1236
1261
 
1237
1262
  // 2. Try priority chain
1238
- for (const pattern of MODEL_PRIO.smol) {
1263
+ for (const pattern of MODEL_PRIO[role]) {
1239
1264
  // Try exact match with provider prefix
1240
1265
  const providerMatch = availableModels.find(m => `${m.provider}/${m.id}`.toLowerCase() === pattern);
1241
- if (providerMatch) return providerMatch;
1266
+ add(providerMatch);
1242
1267
 
1243
1268
  // Try exact match first
1244
1269
  const exactMatch = parseModelPattern(pattern, availableModels, undefined, { modelRegistry }).model;
1245
- if (exactMatch) return exactMatch;
1270
+ add(exactMatch);
1246
1271
 
1247
1272
  // Try fuzzy match (substring)
1248
- const fuzzyMatch = availableModels.find(m => m.id.toLowerCase().includes(pattern));
1249
- if (fuzzyMatch) return fuzzyMatch;
1273
+ const fuzzyMatch = availableModels.find(m => m.id.toLowerCase().includes(pattern.toLowerCase()));
1274
+ add(fuzzyMatch);
1250
1275
  }
1251
1276
 
1252
- // 3. Fallback to first available (same as default)
1253
- return availableModels[0];
1277
+ // 3. Optional full fallback order for retrying consumers.
1278
+ if (includeAllFallbacks) {
1279
+ for (const model of availableModels) add(model);
1280
+ }
1281
+ return candidates;
1254
1282
  }
1255
1283
 
1256
1284
  /**
@@ -1265,26 +1293,5 @@ export async function findSlowModel(
1265
1293
  modelRegistry: ModelLookupRegistry,
1266
1294
  savedModel?: string,
1267
1295
  ): Promise<Model<Api> | undefined> {
1268
- const availableModels = modelRegistry.getAvailable();
1269
- if (availableModels.length === 0) return undefined;
1270
-
1271
- // 1. Try saved model from settings
1272
- if (savedModel) {
1273
- const match = resolveModelFromString(savedModel, availableModels, undefined, modelRegistry);
1274
- if (match) return match;
1275
- }
1276
-
1277
- // 2. Try priority chain
1278
- for (const pattern of MODEL_PRIO.slow) {
1279
- // Try exact match first
1280
- const exactMatch = parseModelPattern(pattern, availableModels, undefined, { modelRegistry }).model;
1281
- if (exactMatch) return exactMatch;
1282
-
1283
- // Try fuzzy match (substring)
1284
- const fuzzyMatch = availableModels.find(m => m.id.toLowerCase().includes(pattern.toLowerCase()));
1285
- if (fuzzyMatch) return fuzzyMatch;
1286
- }
1287
-
1288
- // 3. Fallback to first available (same as default)
1289
- return availableModels[0];
1296
+ return findPriorityRoleModel(modelRegistry, "slow", savedModel);
1290
1297
  }
@@ -544,6 +544,18 @@ export const SETTINGS_SCHEMA = {
544
544
  },
545
545
  },
546
546
 
547
+ "routing.profile": {
548
+ type: "enum",
549
+ values: ["none", "google-antigravity", "openai-codex"] as const,
550
+ default: "none",
551
+ ui: {
552
+ tab: "model",
553
+ label: "Routing Profile",
554
+ description: "Provider-sticky subscription routing profile",
555
+ submenu: true,
556
+ },
557
+ },
558
+
547
559
  "routing.profiler": {
548
560
  type: "enum",
549
561
  values: ["rules", "hybrid"] as const,
@@ -75,16 +75,25 @@ export interface SettingsOptions {
75
75
  // Path Utilities
76
76
  // ═══════════════════════════════════════════════════════════════════════════
77
77
 
78
+ const UNSAFE_PATH_SEGMENTS = new Set(["__proto__", "constructor", "prototype"]);
79
+
80
+ function hasUnsafePathSegment(segments: string[]): boolean {
81
+ return segments.some(segment => segment.length === 0 || UNSAFE_PATH_SEGMENTS.has(segment));
82
+ }
83
+
78
84
  /**
79
85
  * Get a nested value from an object by path segments.
80
86
  */
81
87
  function getByPath(obj: RawSettings, segments: string[]): unknown {
88
+ if (hasUnsafePathSegment(segments)) return undefined;
82
89
  let current: unknown = obj;
83
90
  for (const segment of segments) {
84
91
  if (current === null || current === undefined || typeof current !== "object") {
85
92
  return undefined;
86
93
  }
87
- current = (current as Record<string, unknown>)[segment];
94
+ const record = current as Record<string, unknown>;
95
+ if (!Object.hasOwn(record, segment)) return undefined;
96
+ current = record[segment];
88
97
  }
89
98
  return current;
90
99
  }
@@ -94,15 +103,25 @@ function getByPath(obj: RawSettings, segments: string[]): unknown {
94
103
  * Creates intermediate objects as needed.
95
104
  */
96
105
  function setByPath(obj: RawSettings, segments: string[], value: unknown): void {
106
+ if (hasUnsafePathSegment(segments)) throw new Error("Unsafe settings path");
97
107
  let current = obj;
98
108
  for (let i = 0; i < segments.length - 1; i++) {
99
109
  const segment = segments[i];
100
- if (!(segment in current) || typeof current[segment] !== "object" || current[segment] === null) {
101
- current[segment] = {};
102
- }
103
- current = current[segment] as RawSettings;
104
- }
105
- current[segments[segments.length - 1]] = value;
110
+ const existing = Object.hasOwn(current, segment) ? current[segment] : undefined;
111
+ if (typeof existing === "object" && existing !== null) {
112
+ current = existing as RawSettings;
113
+ continue;
114
+ }
115
+ const child: RawSettings = {};
116
+ Object.defineProperty(current, segment, { configurable: true, enumerable: true, value: child, writable: true });
117
+ current = child;
118
+ }
119
+ Object.defineProperty(current, segments[segments.length - 1], {
120
+ configurable: true,
121
+ enumerable: true,
122
+ value,
123
+ writable: true,
124
+ });
106
125
  }
107
126
 
108
127
  // ═══════════════════════════════════════════════════════════════════════════
@@ -247,13 +266,14 @@ export class Settings {
247
266
  */
248
267
  clearOverride(path: SettingPath): void {
249
268
  const segments = path.split(".");
269
+ if (hasUnsafePathSegment(segments)) throw new Error("Unsafe settings path");
250
270
  let current = this.#overrides;
251
271
  for (let i = 0; i < segments.length - 1; i++) {
252
272
  const segment = segments[i];
253
- if (!(segment in current)) return;
273
+ if (!Object.hasOwn(current, segment)) return;
254
274
  current = current[segment] as RawSettings;
255
275
  }
256
- delete current[segments[segments.length - 1]];
276
+ Reflect.deleteProperty(current, segments[segments.length - 1]);
257
277
  this.#rebuildMerged();
258
278
  }
259
279
 
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "20.14.0",
21
- "commit": "97c02a788b722cbc3aec14d92b37a4c721454687",
22
- "shortCommit": "97c02a7",
20
+ "version": "20.15.2",
21
+ "commit": "1acb1e762fd2e2e9f403ae83cab293e9280a71de",
22
+ "shortCommit": "1acb1e7",
23
23
  "branch": "main",
24
- "tag": "v20.14.0",
25
- "commitDate": "2026-08-11T21:58:20Z",
26
- "buildDate": "2026-08-12T08:05:53.700Z",
24
+ "tag": "v20.15.2",
25
+ "commitDate": "2026-08-13T23:35:16Z",
26
+ "buildDate": "2026-08-13T23:58:59.501Z",
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/97c02a788b722cbc3aec14d92b37a4c721454687",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v20.14.0"
31
+ "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/1acb1e762fd2e2e9f403ae83cab293e9280a71de",
32
+ "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v20.15.2"
33
33
  };
@@ -2,10 +2,10 @@
2
2
 
3
3
  import type { ConsoleCatalogData } from "./console-catalog-types";
4
4
 
5
- export const CONSOLE_CATALOG_VERSION = "4450d0ec7d164aaee226271a6b4cd69aca408f28";
5
+ export const CONSOLE_CATALOG_VERSION = "8b9b10a8d85e70ddbb46cce584fcaa5fdf2b12c3";
6
6
 
7
7
  export const CONSOLE_CATALOG_DATA: ConsoleCatalogData = {
8
- version: "4450d0ec7d164aaee226271a6b4cd69aca408f28",
8
+ version: "8b9b10a8d85e70ddbb46cce584fcaa5fdf2b12c3",
9
9
  workflows: {
10
10
  "address-allocator/create":
11
11
  '---\nschema: urn:xcsh:console:workflow:v1\nid: address-allocator-create\nlabel: Create IP Address Allocators\nresource: address-allocator\noperation: create\npreconditions:\n - user_logged_in\n - "role_minimum: admin"\nparams:\n name:\n required: true\n description: IP Address Allocators name (lowercase alphanumeric and hyphens)\n example: example-address-allocator\n address_allocator_mode:\n required: true\n description: Address Allocator Mode\n allocation_unit:\n required: false\n description: Allocation Unit\n default: 0\n address_pool:\n required: false\n description: Address Pool\n default: value\n address_allocation_scheme:\n required: false\n description: "Server-required: Field should be not nil"\n default: value\nsteps:\n - id: navigate-to-list\n action: navigate\n url: /web/workspaces/multi-cloud-network-connect/manage/networking/legacy_network_configuration/address_allocators\n wait_for: text(\'IP Address Allocators\')\n description: Navigate to IP Address Allocators list page\n - id: click-add-tab\n action: click\n selector: text(\'Add IP Address Allocator\')\n wait_for: textbox[name=\'Name\']\n description: Click Add IP Address Allocator to open the create form\n - id: fill-name\n action: fill\n selector: textbox[name=\'Name\']\n value: "{name}"\n description: Enter Name\n - id: select-address_allocator_mode\n action: select\n selector: listbox\n context: Address Allocator Mode section\n value: "{address_allocator_mode}"\n description: Select Address Allocator Mode\n - id: fill-allocation_unit\n action: fill\n selector: spinbutton[name=\'Allocation Unit\']\n value: "{allocation_unit}"\n description: Set Allocation Unit\n - id: fill-address_pool\n action: fill\n selector: ngx-datatable input.form-control\n context: Address Pool table\n value: "{address_pool}"\n description: Enter Address Pool in the existing table row (no Add Item needed — the table ships one empty row)\n - id: select-address_allocation_scheme\n action: select\n selector: listbox\n context: Address Allocation Scheme section\n value: "{address_allocation_scheme}"\n description: Select Address Allocation Scheme\n - id: save\n action: click\n selector: "[class*=\'save-bt\'],[class*=\'submit-button\']"\n context: footer\n wait_for: text(\'{name}\')\n wait_timeout_ms: 30000\n description: Save/submit the form (union selector matches save-bt OR submit-button)\npostconditions:\n - resource_list_page_visible\n - "resource_name_in_list: {name}"\nmetadata:\n confidence: inferred\n discovered_at: 2026-06-24\n console_version: "2025.06"\n notes: Auto-generated by scripts/generate-workflows.ts from api-specs-enriched field metadata.\n',