@f5xc-salesdemos/xcsh 19.8.0 → 19.9.0

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.
@@ -118,7 +118,7 @@ export default function (pi: ExtensionAPI) {
118
118
  for (const entry of ctx.sessionManager.getBranch()) {
119
119
  if (entry.type !== "message") continue;
120
120
  const msg = (entry as { message?: { role?: string; toolName?: string; details?: unknown } }).message;
121
- if (!msg || msg.role !== "toolResult" || msg.toolName !== "todo") continue;
121
+ if (msg?.role !== "toolResult" || msg.toolName !== "todo") continue;
122
122
 
123
123
  const details = msg.details as TodoDetails | undefined;
124
124
  if (details) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5xc-salesdemos/xcsh",
4
- "version": "19.8.0",
4
+ "version": "19.9.0",
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",
@@ -50,12 +50,12 @@
50
50
  "dependencies": {
51
51
  "@agentclientprotocol/sdk": "0.16.1",
52
52
  "@mozilla/readability": "^0.6",
53
- "@f5xc-salesdemos/xcsh-stats": "19.8.0",
54
- "@f5xc-salesdemos/pi-agent-core": "19.8.0",
55
- "@f5xc-salesdemos/pi-ai": "19.8.0",
56
- "@f5xc-salesdemos/pi-natives": "19.8.0",
57
- "@f5xc-salesdemos/pi-tui": "19.8.0",
58
- "@f5xc-salesdemos/pi-utils": "19.8.0",
53
+ "@f5xc-salesdemos/xcsh-stats": "19.9.0",
54
+ "@f5xc-salesdemos/pi-agent-core": "19.9.0",
55
+ "@f5xc-salesdemos/pi-ai": "19.9.0",
56
+ "@f5xc-salesdemos/pi-natives": "19.9.0",
57
+ "@f5xc-salesdemos/pi-tui": "19.9.0",
58
+ "@f5xc-salesdemos/pi-utils": "19.9.0",
59
59
  "@sinclair/typebox": "^0.34",
60
60
  "@xterm/headless": "^6.0",
61
61
  "ajv": "^8.20",
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "19.8.0",
21
- "commit": "ea6c3e825dbbdf4e3e471bbe3a22dc503e00f30e",
22
- "shortCommit": "ea6c3e8",
20
+ "version": "19.9.0",
21
+ "commit": "146551aadd2d3231eb436e0f5f760c96dc3ee602",
22
+ "shortCommit": "146551a",
23
23
  "branch": "main",
24
- "tag": "v19.8.0",
25
- "commitDate": "2026-06-05T00:33:42Z",
26
- "buildDate": "2026-06-05T00:57:39.846Z",
24
+ "tag": "v19.9.0",
25
+ "commitDate": "2026-06-05T02:16:52Z",
26
+ "buildDate": "2026-06-05T02:38:22.277Z",
27
27
  "dirty": true,
28
28
  "prNumber": "",
29
29
  "repoUrl": "https://github.com/f5xc-salesdemos/xcsh",
30
30
  "repoSlug": "f5xc-salesdemos/xcsh",
31
- "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/ea6c3e825dbbdf4e3e471bbe3a22dc503e00f30e",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.8.0"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/146551aadd2d3231eb436e0f5f760c96dc3ee602",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.9.0"
33
33
  };
package/src/lsp/render.ts CHANGED
@@ -110,7 +110,7 @@ export function renderResult(
110
110
  args?: LspParams,
111
111
  ): Component {
112
112
  const content = result.content?.[0];
113
- if (!content || content.type !== "text" || !("text" in content) || !content.text) {
113
+ if (content?.type !== "text" || !("text" in content) || !content.text) {
114
114
  const icon = formatStatusIcon("warning", theme, options.spinnerFrame);
115
115
  const header = `${icon} LSP`;
116
116
  return new Text([header, theme.fg("dim", "No result")].join("\n"), 0, 0);
@@ -42,7 +42,7 @@ function getUriPort(uri: URL): number {
42
42
 
43
43
  function validateRedirectConfig(config: MCPOAuthConfig, redirectUri: string | undefined): void {
44
44
  const parsed = parseRedirectUri(redirectUri);
45
- if (!parsed || parsed.protocol !== "https:" || !isLoopbackHostname(parsed.hostname)) {
45
+ if (parsed?.protocol !== "https:" || !isLoopbackHostname(parsed.hostname)) {
46
46
  return;
47
47
  }
48
48
 
@@ -63,7 +63,7 @@ function resolveCallbackPort(callbackPort: number | undefined, redirectUri: stri
63
63
  if (callbackPort !== undefined) return callbackPort;
64
64
 
65
65
  const parsed = parseRedirectUri(redirectUri);
66
- if (!parsed || parsed.protocol !== "http:" || !isLoopbackHostname(parsed.hostname)) {
66
+ if (parsed?.protocol !== "http:" || !isLoopbackHostname(parsed.hostname)) {
67
67
  return DEFAULT_PORT;
68
68
  }
69
69
 
@@ -121,7 +121,7 @@ function matchAgent(agent: DashboardAgent, query: string): boolean {
121
121
  function extractAssistantText(messages: AgentMessage[]): string | null {
122
122
  for (let i = messages.length - 1; i >= 0; i--) {
123
123
  const message = messages[i];
124
- if (!message || message.role !== "assistant") continue;
124
+ if (message?.role !== "assistant") continue;
125
125
  const blocks = message.content;
126
126
  if (!Array.isArray(blocks)) continue;
127
127
  const text = blocks
@@ -132,7 +132,7 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
132
132
  const removedLines: { lineNum: string; content: string }[] = [];
133
133
  while (i < lines.length) {
134
134
  const p = parseDiffLine(lines[i]);
135
- if (!p || p.prefix !== "-") break;
135
+ if (p?.prefix !== "-") break;
136
136
  removedLines.push({ lineNum: p.lineNum, content: p.content });
137
137
  i++;
138
138
  }
@@ -141,7 +141,7 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
141
141
  const addedLines: { lineNum: string; content: string }[] = [];
142
142
  while (i < lines.length) {
143
143
  const p = parseDiffLine(lines[i]);
144
- if (!p || p.prefix !== "+") break;
144
+ if (p?.prefix !== "+") break;
145
145
  addedLines.push({ lineNum: p.lineNum, content: p.content });
146
146
  i++;
147
147
  }
@@ -251,7 +251,7 @@ export class PluginDashboard extends Container {
251
251
  if (!plugin) return;
252
252
  const tabId = this.#activeTabId();
253
253
 
254
- if (tabId === "discover" && !plugin.installed) {
254
+ if ((tabId === "discover" || tabId === "recommended") && !plugin.installed) {
255
255
  await this.#installPlugin(plugin);
256
256
  } else if (tabId === "updates" && plugin.hasUpdate) {
257
257
  await this.#upgradePlugin(plugin);
@@ -289,6 +289,35 @@ export class PluginDashboard extends Container {
289
289
  }
290
290
  }
291
291
 
292
+ async #installAllRecommended(): Promise<void> {
293
+ const recommended = this.#state.allPlugins.filter(p => !p.installed && p.recommended && p.marketplace);
294
+ if (recommended.length === 0) {
295
+ this.#state.notice = "All recommended plugins are already installed";
296
+ this.#rebuildAndRender();
297
+ return;
298
+ }
299
+
300
+ this.#state.notice = `Installing ${recommended.length} recommended plugin(s)...`;
301
+ this.#rebuildAndRender();
302
+
303
+ let installed = 0;
304
+ let failed = 0;
305
+ for (const plugin of recommended) {
306
+ try {
307
+ await this.#mgr.installPlugin(plugin.name, plugin.marketplace!);
308
+ installed++;
309
+ } catch {
310
+ failed++;
311
+ }
312
+ }
313
+
314
+ this.#state.notice =
315
+ failed > 0
316
+ ? `Installed ${installed}/${recommended.length} recommended plugin(s), ${failed} failed`
317
+ : `Installed ${installed} recommended plugin(s)`;
318
+ await this.#reloadData();
319
+ }
320
+
292
321
  async #upgradePlugin(plugin: DashboardPlugin): Promise<void> {
293
322
  this.#state.notice = `Upgrading ${plugin.name}...`;
294
323
  this.#rebuildAndRender();
@@ -320,6 +349,8 @@ export class PluginDashboard extends Container {
320
349
  #getHelpText(): string {
321
350
  const tabId = this.#activeTabId();
322
351
  switch (tabId) {
352
+ case "recommended":
353
+ return " ↑/↓: navigate Enter: install A: install all Tab: next tab Ctrl+R: reload Esc: close";
323
354
  case "discover":
324
355
  return " ↑/↓: navigate Enter: install Tab: next tab Ctrl+R: reload Esc: close";
325
356
  case "updates":
@@ -421,6 +452,11 @@ export class PluginDashboard extends Container {
421
452
  return;
422
453
  }
423
454
 
455
+ if (data.toLowerCase() === "a" && this.#activeTabId() === "recommended") {
456
+ void this.#installAllRecommended();
457
+ return;
458
+ }
459
+
424
460
  if (data.toLowerCase() === "u") {
425
461
  const plugin = this.#selectedPlugin();
426
462
  if (plugin?.hasUpdate) {
@@ -24,9 +24,11 @@ export class PluginListPane implements Component {
24
24
  const msg =
25
25
  this.activeTab === "discover"
26
26
  ? "No plugins available. Add a marketplace first."
27
- : this.activeTab === "updates"
28
- ? "All plugins are up to date."
29
- : "No plugins installed.";
27
+ : this.activeTab === "recommended"
28
+ ? "All recommended plugins are installed."
29
+ : this.activeTab === "updates"
30
+ ? "All plugins are up to date."
31
+ : "No plugins installed.";
30
32
  lines.push(theme.fg("muted", ` ${msg}`));
31
33
  return lines;
32
34
  }
@@ -66,6 +68,8 @@ export class PluginListPane implements Component {
66
68
  } else {
67
69
  parts.push(theme.fg("dim", theme.status.disabled));
68
70
  }
71
+ } else if (plugin.recommended) {
72
+ parts.push(theme.fg("warning", "*"));
69
73
  } else {
70
74
  parts.push(theme.fg("dim", "·"));
71
75
  }
@@ -22,3 +22,34 @@ API calls to the same F5 XC tenant reuse a single TLS connection — sequential
22
22
 
23
23
  **Relationship queries**: When the batch response says "Inventory complete" and includes a `Resource relationships:` section, the specs and relationships are already fully fetched. Answer directly from that data. Do NOT make additional GET calls to read individual resources you already have from the batch.
24
24
  **Tenant-wide queries**: When asked about resources across ALL namespaces (e.g. "show all LBs in the entire tenant"), use `paths: ["*"]` with `params: {namespace: "*"}` to batch every namespace in ONE call. Do NOT list namespaces first — the wildcard handles discovery automatically.
25
+
26
+ **HTTP load balancer with origin pool** — when asked to create an LB routing to a named pool, use this exact payload structure (POST to `http_loadbalancers`):
27
+
28
+ ```json
29
+ {
30
+ "metadata": { "name": "<lb-name>", "namespace": "<ns>" },
31
+ "spec": {
32
+ "domains": ["<domain>"],
33
+ "advertise_on_public_default_vip": {},
34
+ "http": { "port": 80 },
35
+ "default_route_pools": [
36
+ { "pool": { "namespace": "<ns>", "name": "<pool-name>" }, "weight": 1, "priority": 1 }
37
+ ]
38
+ }
39
+ }
40
+ ```
41
+
42
+ For HTTPS: replace `"http": {"port": 80}` with `"https_auto_cert": {"http_redirect": true, "default_header": {}, "tls_config": {"default_security": {}}, "no_mtls": {}}`. For no pool (advertise only): omit `default_route_pools`.
43
+
44
+ **Resource disambiguation**: Several F5 XC resource types have similar names but different API paths. When the user's intent maps to one of these, use the exact API path shown:
45
+
46
+ |User says|Catalog category|API path segment|NOT|
47
+ |---|---|---|---|
48
+ |"rate limiter policy"|`rate-limiter-policys`|`rate_limiter_policys`|`policers`, `rate_limiters`|
49
+ |"policer"|`policers`|`policers`|`rate_limiter_policys`|
50
+ |"rate limiter"|`rate-limiters`|`rate_limiters`|`rate_limiter_policys`, `policers`|
51
+
52
+ **payload schemas for rate-limiting resources:**
53
+ - `policers` / "policer": `{"metadata":{"name":"<n>","namespace":"<ns>"},"spec":{"burst_size":<int>,"committed_information_rate":<int>}}` — network-level byte/Mbps limiting
54
+ - `rate_limiters` / "rate limiter": `{"metadata":{"name":"<n>","namespace":"<ns>"},"spec":{"burst_size":<int>,"committed_information_rate":<int>}}` — HTTP request-level limiting (rps)
55
+ - `rate_limiter_policys` / "rate limiter policy": requires existing `rate_limiter` reference; use `{"metadata":{"name":"<n>","namespace":"<ns>"},"spec":{"any_server":{},"rules":[{"metadata":{"name":"r"},"spec":{"any_client":{},"any_ip":{},"rate_limiter":{"namespace":"<ns>","name":"<rl-name>"}}}]}}`
@@ -4517,7 +4517,7 @@ export class AgentSession {
4517
4517
 
4518
4518
  #closeCodexProviderSessionsForHistoryRewrite(): void {
4519
4519
  const currentModel = this.model;
4520
- if (!currentModel || currentModel.api !== "openai-codex-responses") return;
4520
+ if (currentModel?.api !== "openai-codex-responses") return;
4521
4521
  this.#closeProviderSessionsForModelSwitch(currentModel, currentModel);
4522
4522
  }
4523
4523
 
@@ -6115,7 +6115,7 @@ export class AgentSession {
6115
6115
  const previousSessionFile = this.sessionFile;
6116
6116
  const selectedEntry = this.sessionManager.getEntry(entryId);
6117
6117
 
6118
- if (!selectedEntry || selectedEntry.type !== "message" || selectedEntry.message.role !== "user") {
6118
+ if (selectedEntry?.type !== "message" || selectedEntry.message.role !== "user") {
6119
6119
  throw new Error("Invalid entry ID for branching");
6120
6120
  }
6121
6121
 
package/src/vim/engine.ts CHANGED
@@ -867,7 +867,7 @@ export class VimEngine {
867
867
  }
868
868
  case "r": {
869
869
  const replacement = tokens[nextIndex + 1];
870
- if (!replacement || replacement.value.length !== 1) {
870
+ if (replacement?.value.length !== 1) {
871
871
  throw new VimError("Visual replace requires a literal character", opToken);
872
872
  }
873
873
  const visual = expandVisualOffsets(
@@ -1134,7 +1134,7 @@ export class VimEngine {
1134
1134
  return nextIndex + 1;
1135
1135
  case "r": {
1136
1136
  const replacement = tokens[nextIndex + 1];
1137
- if (!replacement || replacement.value.length !== 1) {
1137
+ if (replacement?.value.length !== 1) {
1138
1138
  throw new VimError("r requires a replacement character", token);
1139
1139
  }
1140
1140
  await this.#applyAtomicChange(["r", replacement.value], () => {
@@ -1763,7 +1763,7 @@ export class VimEngine {
1763
1763
  case "t":
1764
1764
  case "T": {
1765
1765
  const searchToken = tokens[index + 1];
1766
- if (!searchToken || searchToken.value.length !== 1) {
1766
+ if (searchToken?.value.length !== 1) {
1767
1767
  throw new VimError(`${token.value} requires a literal character`, token);
1768
1768
  }
1769
1769
  this.lastCharFind = { char: searchToken.value, mode: token.value as "f" | "F" | "t" | "T" };