@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 +8 -8
- package/src/commit/model-selection.ts +3 -6
- package/src/config/model-registry.ts +18 -1
- package/src/config/model-resolver.ts +38 -31
- package/src/config/settings-schema.ts +12 -0
- package/src/config/settings.ts +29 -9
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/console-catalog.generated.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/modes/controllers/login-model.ts +72 -5
- package/src/routing/commands.ts +15 -1
- package/src/routing/coordinator.ts +11 -0
- package/src/routing/effort.ts +19 -1
- package/src/routing/index.ts +1 -0
- package/src/routing/presets.ts +2 -0
- package/src/routing/subscription-profiles.ts +82 -0
- package/src/routing/types.ts +14 -0
- package/src/session/agent-session.ts +60 -11
- package/src/slash-commands/builtin-registry.ts +8 -0
- package/src/sweep/spec-probe.ts +14 -5
- package/src/task/executor.ts +1 -1
- package/src/tools/fetch.ts +7 -4
- package/src/tools/submit-result.ts +1 -1
- package/src/utils/commit-message-generator.ts +2 -9
- package/src/web/scrapers/chocolatey.ts +9 -4
- package/src/web/scrapers/hackage.ts +3 -4
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/web/search/providers/exa.ts +4 -4
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "20.
|
|
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.
|
|
65
|
-
"@f5-sales-demo/pi-ai": "20.
|
|
66
|
-
"@f5-sales-demo/pi-natives": "20.
|
|
67
|
-
"@f5-sales-demo/pi-resource-management": "20.
|
|
68
|
-
"@f5-sales-demo/pi-tui": "20.
|
|
69
|
-
"@f5-sales-demo/pi-utils": "20.
|
|
70
|
-
"@f5-sales-demo/xcsh-stats": "20.
|
|
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
|
|
58
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1259
|
+
add(match);
|
|
1235
1260
|
}
|
|
1236
1261
|
|
|
1237
1262
|
// 2. Try priority chain
|
|
1238
|
-
for (const pattern of MODEL_PRIO
|
|
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
|
-
|
|
1266
|
+
add(providerMatch);
|
|
1242
1267
|
|
|
1243
1268
|
// Try exact match first
|
|
1244
1269
|
const exactMatch = parseModelPattern(pattern, availableModels, undefined, { modelRegistry }).model;
|
|
1245
|
-
|
|
1270
|
+
add(exactMatch);
|
|
1246
1271
|
|
|
1247
1272
|
// Try fuzzy match (substring)
|
|
1248
|
-
const fuzzyMatch = availableModels.find(m => m.id.toLowerCase().includes(pattern));
|
|
1249
|
-
|
|
1273
|
+
const fuzzyMatch = availableModels.find(m => m.id.toLowerCase().includes(pattern.toLowerCase()));
|
|
1274
|
+
add(fuzzyMatch);
|
|
1250
1275
|
}
|
|
1251
1276
|
|
|
1252
|
-
// 3.
|
|
1253
|
-
|
|
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
|
-
|
|
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,
|
package/src/config/settings.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
273
|
+
if (!Object.hasOwn(current, segment)) return;
|
|
254
274
|
current = current[segment] as RawSettings;
|
|
255
275
|
}
|
|
256
|
-
|
|
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.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "20.15.2",
|
|
21
|
+
"commit": "1acb1e762fd2e2e9f403ae83cab293e9280a71de",
|
|
22
|
+
"shortCommit": "1acb1e7",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v20.
|
|
25
|
-
"commitDate": "2026-08-
|
|
26
|
-
"buildDate": "2026-08-
|
|
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/
|
|
32
|
-
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v20.
|
|
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 = "
|
|
5
|
+
export const CONSOLE_CATALOG_VERSION = "8b9b10a8d85e70ddbb46cce584fcaa5fdf2b12c3";
|
|
6
6
|
|
|
7
7
|
export const CONSOLE_CATALOG_DATA: ConsoleCatalogData = {
|
|
8
|
-
version: "
|
|
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',
|