@f5xc-salesdemos/xcsh 19.19.3 → 19.20.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5xc-salesdemos/xcsh",
4
- "version": "19.19.3",
4
+ "version": "19.20.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.19.3",
54
- "@f5xc-salesdemos/pi-agent-core": "19.19.3",
55
- "@f5xc-salesdemos/pi-ai": "19.19.3",
56
- "@f5xc-salesdemos/pi-natives": "19.19.3",
57
- "@f5xc-salesdemos/pi-tui": "19.19.3",
58
- "@f5xc-salesdemos/pi-utils": "19.19.3",
53
+ "@f5xc-salesdemos/xcsh-stats": "19.20.0",
54
+ "@f5xc-salesdemos/pi-agent-core": "19.20.0",
55
+ "@f5xc-salesdemos/pi-ai": "19.20.0",
56
+ "@f5xc-salesdemos/pi-natives": "19.20.0",
57
+ "@f5xc-salesdemos/pi-tui": "19.20.0",
58
+ "@f5xc-salesdemos/pi-utils": "19.20.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.19.3",
21
- "commit": "0d01a928658034ee23d053d44eaed4463ee35b8a",
22
- "shortCommit": "0d01a92",
20
+ "version": "19.20.0",
21
+ "commit": "218c1d5ef18212bd9c034ea86f80927094c941c1",
22
+ "shortCommit": "218c1d5",
23
23
  "branch": "main",
24
- "tag": "v19.19.3",
25
- "commitDate": "2026-06-09T02:04:30Z",
26
- "buildDate": "2026-06-09T02:27:32.159Z",
24
+ "tag": "v19.20.0",
25
+ "commitDate": "2026-06-09T03:36:14Z",
26
+ "buildDate": "2026-06-09T04:39:54.571Z",
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/0d01a928658034ee23d053d44eaed4463ee35b8a",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.19.3"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/218c1d5ef18212bd9c034ea86f80927094c941c1",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.20.0"
33
33
  };
@@ -228,6 +228,95 @@ export interface RecommendedPluginStatus {
228
228
  installed: boolean;
229
229
  }
230
230
 
231
+ export type UnifiedPluginState = "connected" | "unauthenticated" | "unavailable" | "installed" | "not_installed";
232
+
233
+ export interface UnifiedPluginStatus {
234
+ name: string;
235
+ state: UnifiedPluginState;
236
+ hint?: string;
237
+ group?: string;
238
+ }
239
+
240
+ export interface ServiceStatusContributionInput {
241
+ name: string;
242
+ group?: string;
243
+ check: () => Promise<{ state: ServiceState; hint?: string }>;
244
+ }
245
+
246
+ export async function buildUnifiedPluginList(
247
+ contributions: ServiceStatusContributionInput[],
248
+ ): Promise<UnifiedPluginStatus[]> {
249
+ const map = new Map<string, UnifiedPluginStatus>();
250
+
251
+ for (const contribution of contributions) {
252
+ try {
253
+ const status = await contribution.check();
254
+ map.set(contribution.name.toLowerCase(), {
255
+ name: contribution.name,
256
+ state: status.state,
257
+ hint: status.hint,
258
+ group: contribution.group,
259
+ });
260
+ } catch {
261
+ map.set(contribution.name.toLowerCase(), {
262
+ name: contribution.name,
263
+ state: "unavailable",
264
+ hint: "check failed",
265
+ group: contribution.group,
266
+ });
267
+ }
268
+ }
269
+
270
+ try {
271
+ const mgr = new MarketplaceManager({
272
+ marketplacesRegistryPath: getMarketplacesRegistryPath(),
273
+ installedRegistryPath: getInstalledPluginsRegistryPath(),
274
+ marketplacesCacheDir: getMarketplacesCacheDir(),
275
+ pluginsCacheDir: getPluginsCacheDir(),
276
+ clearPluginRootsCache: () => {},
277
+ });
278
+
279
+ const [marketplaces, installedSummaries] = await Promise.all([
280
+ mgr.listMarketplaces(),
281
+ mgr.listInstalledPlugins(),
282
+ ]);
283
+
284
+ const installedIds = new Set(installedSummaries.map(s => s.id));
285
+
286
+ for (const mkt of marketplaces) {
287
+ const available = await mgr.listAvailablePlugins(mkt.name).catch(() => []);
288
+ for (const entry of available) {
289
+ if (!entry.recommended) continue;
290
+ const displayName = normalizePluginDisplayName(entry.name);
291
+ const key = displayName.toLowerCase();
292
+ if (map.has(key)) continue;
293
+ const pluginId = `${entry.name}@${mkt.name}`;
294
+ map.set(key, {
295
+ name: displayName,
296
+ state: installedIds.has(pluginId) ? "installed" : "not_installed",
297
+ hint: installedIds.has(pluginId) ? undefined : "run: /plugin setup",
298
+ });
299
+ }
300
+ }
301
+ } catch (err) {
302
+ logger.debug("buildUnifiedPluginList marketplace check failed", { error: String(err) });
303
+ }
304
+
305
+ const stateOrder: Record<UnifiedPluginState, number> = {
306
+ connected: 0,
307
+ unauthenticated: 1,
308
+ unavailable: 2,
309
+ installed: 3,
310
+ not_installed: 4,
311
+ };
312
+
313
+ return [...map.values()].sort((a, b) => {
314
+ const orderDiff = stateOrder[a.state] - stateOrder[b.state];
315
+ if (orderDiff !== 0) return orderDiff;
316
+ return a.name.localeCompare(b.name);
317
+ });
318
+ }
319
+
231
320
  export async function checkRecommendedPlugins(): Promise<RecommendedPluginStatus[]> {
232
321
  try {
233
322
  const mgr = new MarketplaceManager({
@@ -2,7 +2,7 @@ import { type Component, padding, truncateToWidth, visibleWidth } from "@f5xc-sa
2
2
  import { APP_NAME } from "@f5xc-salesdemos/pi-utils";
3
3
  import { theme } from "../../modes/theme/theme";
4
4
  import { formatStatusIcon } from "../../services/f5xc-context-indicators";
5
- import type { ModelStatus, RecommendedPluginStatus, ServiceStatus } from "./welcome-checks";
5
+ import type { ModelStatus, RecommendedPluginStatus, ServiceStatus, UnifiedPluginStatus } from "./welcome-checks";
6
6
 
7
7
  export interface UpdateStatus {
8
8
  available: boolean;
@@ -15,7 +15,8 @@ export class WelcomeComponent implements Component {
15
15
  private modelStatus: ModelStatus,
16
16
  private services: ServiceStatus[] = [],
17
17
  private updateStatus?: UpdateStatus,
18
- private recommendedPlugins: RecommendedPluginStatus[] = [],
18
+ private _recommendedPlugins: RecommendedPluginStatus[] = [],
19
+ private plugins: UnifiedPluginStatus[] = [],
19
20
  ) {}
20
21
  invalidate(): void {}
21
22
  setModelStatus(status: ModelStatus): void {
@@ -28,7 +29,10 @@ export class WelcomeComponent implements Component {
28
29
  this.updateStatus = status;
29
30
  }
30
31
  setRecommendedPlugins(plugins: RecommendedPluginStatus[]): void {
31
- this.recommendedPlugins = plugins;
32
+ this._recommendedPlugins = plugins;
33
+ }
34
+ setPlugins(plugins: UnifiedPluginStatus[]): void {
35
+ this.plugins = plugins;
32
36
  }
33
37
 
34
38
  render(termWidth: number): string[] {
@@ -127,23 +131,13 @@ export class WelcomeComponent implements Component {
127
131
  #measureStatusWidth(): number {
128
132
  const lines: string[] = [" Model Provider", ...this.#renderModelStatus()];
129
133
  const coreServices = this.services.filter(s => !s._isPlugin);
130
- const pluginServices = this.services.filter(s => s._isPlugin);
131
134
  for (const svc of coreServices) {
132
135
  lines.push(this.#renderServiceLine(svc));
133
136
  }
134
- if (pluginServices.length > 0) {
135
- const groups = this.#groupPluginServices(pluginServices);
136
- for (const [groupName] of groups) {
137
- lines.push(` ${groupName}`);
138
- }
139
- for (const svc of pluginServices) {
140
- lines.push(this.#renderServiceLine(svc));
141
- }
142
- }
143
- if (this.recommendedPlugins.length > 0) {
144
- lines.push(" Recommended Plugins");
145
- for (const p of this.recommendedPlugins) {
146
- lines.push(this.#renderRecommendedLine(p));
137
+ if (this.plugins.length > 0) {
138
+ lines.push(" Plugins");
139
+ for (const p of this.plugins) {
140
+ lines.push(this.#renderUnifiedPluginLine(p));
147
141
  }
148
142
  }
149
143
  if (this.#showUpdateSection()) {
@@ -152,15 +146,15 @@ export class WelcomeComponent implements Component {
152
146
  return Math.max(...lines.map(l => visibleWidth(l)));
153
147
  }
154
148
 
155
- #groupPluginServices(plugins: ServiceStatus[]): Map<string, ServiceStatus[]> {
156
- const groups = new Map<string, ServiceStatus[]>();
157
- for (const svc of plugins) {
158
- const groupName = svc._group ?? "Plugins";
159
- const list = groups.get(groupName) ?? [];
160
- list.push(svc);
161
- groups.set(groupName, list);
149
+ #renderUnifiedPluginLine(plugin: UnifiedPluginStatus): string {
150
+ if (plugin.state === "connected" || plugin.state === "installed") {
151
+ return ` ${formatStatusIcon("connected")} ${theme.fg("muted", plugin.name)}`;
152
+ }
153
+ if (plugin.state === "not_installed") {
154
+ return ` ${theme.fg("dim", "·")} ${theme.fg("dim", plugin.name)}`;
162
155
  }
163
- return groups;
156
+ const hint = plugin.hint ?? "";
157
+ return ` ${formatStatusIcon("warning")} ${theme.fg("muted", plugin.name)} ${theme.fg("dim", hint)}`;
164
158
  }
165
159
 
166
160
  #buildStatusLines(rightCol: number): string[] {
@@ -172,34 +166,19 @@ export class WelcomeComponent implements Component {
172
166
  lines.push(...this.#renderModelStatus());
173
167
  lines.push("");
174
168
  const coreServices = this.services.filter(s => !s._isPlugin);
175
- const pluginServices = this.services.filter(s => s._isPlugin);
176
- const hasContent =
177
- coreServices.length > 0 ||
178
- pluginServices.length > 0 ||
179
- this.recommendedPlugins.length > 0 ||
180
- this.#showUpdateSection();
169
+ const hasContent = coreServices.length > 0 || this.plugins.length > 0 || this.#showUpdateSection();
181
170
  if (hasContent) {
182
171
  lines.push(separator);
183
172
  for (const svc of coreServices) {
184
173
  lines.push(this.#renderServiceLine(svc));
185
174
  }
186
- if (pluginServices.length > 0) {
187
- const groups = this.#groupPluginServices(pluginServices);
188
- for (const [groupName, groupServices] of groups) {
189
- lines.push("");
190
- lines.push(` ${theme.fg("dim", groupName)}`);
191
- for (const svc of groupServices) {
192
- lines.push(this.#renderServiceLine(svc));
193
- }
194
- }
195
- }
196
- if (this.recommendedPlugins.length > 0) {
175
+ if (this.plugins.length > 0) {
197
176
  lines.push("");
198
- lines.push(` ${theme.fg("dim", "Recommended Plugins")}`);
199
- for (const p of this.recommendedPlugins) {
200
- lines.push(this.#renderRecommendedLine(p));
177
+ lines.push(` ${theme.fg("dim", "Plugins")}`);
178
+ for (const p of this.plugins) {
179
+ lines.push(this.#renderUnifiedPluginLine(p));
201
180
  }
202
- const missing = this.recommendedPlugins.filter(p => !p.installed);
181
+ const missing = this.plugins.filter(p => p.state === "not_installed");
203
182
  if (missing.length > 0) {
204
183
  lines.push(` ${theme.fg("dim", "run: /plugin setup")}`);
205
184
  }
@@ -213,13 +192,6 @@ export class WelcomeComponent implements Component {
213
192
  return lines;
214
193
  }
215
194
 
216
- #renderRecommendedLine(plugin: RecommendedPluginStatus): string {
217
- if (plugin.installed) {
218
- return ` ${formatStatusIcon("connected")} ${theme.fg("muted", plugin.name)}`;
219
- }
220
- return ` ${theme.fg("dim", "·")} ${theme.fg("dim", plugin.name)}`;
221
- }
222
-
223
195
  #showUpdateSection(): boolean {
224
196
  return this.updateStatus?.available === true;
225
197
  }
@@ -53,7 +53,7 @@ import { StatusLineComponent } from "./components/status-line";
53
53
  import type { ToolExecutionHandle } from "./components/tool-execution";
54
54
  import { type UpdateStatus, WelcomeComponent } from "./components/welcome";
55
55
  import {
56
- checkRecommendedPlugins,
56
+ buildUnifiedPluginList,
57
57
  type FixableService,
58
58
  mapContextStatus,
59
59
  runWelcomeChecks,
@@ -340,64 +340,40 @@ export class InteractiveMode implements InteractiveModeContext {
340
340
  ? [mapContextStatus(welcomeResult.context ?? { state: "no_context" })]
341
341
  : [];
342
342
 
343
- // Collect service statuses from plugins
344
- if (this.session.extensionRunner) {
345
- const pluginContributions = this.session.extensionRunner.getAllRegisteredServiceStatuses();
346
- for (const contribution of pluginContributions) {
347
- try {
348
- const status = await contribution.check();
349
- services.push({ name: contribution.name, ...status, _isPlugin: true, _group: contribution.group });
350
- } catch {
351
- services.push({
352
- name: contribution.name,
353
- state: "unavailable",
354
- hint: "check failed",
355
- _isPlugin: true,
356
- _group: contribution.group,
357
- });
358
- }
359
- }
360
- }
343
+ // Build unified plugin list from service status contributions + marketplace
344
+ const pluginContributions = this.session.extensionRunner?.getAllRegisteredServiceStatuses() ?? [];
345
+ const plugins = !startupQuiet ? await buildUnifiedPluginList(pluginContributions).catch(() => []) : [];
361
346
 
362
347
  const fixableServices: FixableService[] = [];
363
-
364
- // Add fixable services from plugins
365
- if (this.session.extensionRunner) {
366
- const pluginContributions = this.session.extensionRunner.getAllRegisteredServiceStatuses();
367
- for (const contribution of pluginContributions) {
368
- if (contribution.fix) {
369
- // Find the status we already checked above
370
- const status = services.find(s => s.name === contribution.name);
371
- if (status && status.state === "unauthenticated") {
372
- fixableServices.push({
373
- name: contribution.name,
374
- prompt: contribution.fix.prompt,
375
- command: contribution.fix.command,
376
- recheck: async () => {
377
- try {
378
- const result = await contribution.check();
379
- return { name: contribution.name, ...result };
380
- } catch {
381
- return { name: contribution.name, state: "unavailable" as const, hint: "recheck failed" };
382
- }
383
- },
384
- });
385
- }
348
+ for (const contribution of pluginContributions) {
349
+ if (contribution.fix) {
350
+ const plugin = plugins.find(p => p.name.toLowerCase() === contribution.name.toLowerCase());
351
+ if (plugin && plugin.state === "unauthenticated") {
352
+ fixableServices.push({
353
+ name: contribution.name,
354
+ prompt: contribution.fix.prompt,
355
+ command: contribution.fix.command,
356
+ recheck: async () => {
357
+ try {
358
+ const result = await contribution.check();
359
+ return { name: contribution.name, ...result };
360
+ } catch {
361
+ return { name: contribution.name, state: "unavailable" as const, hint: "recheck failed" };
362
+ }
363
+ },
364
+ });
386
365
  }
387
366
  }
388
367
  }
389
368
 
390
- const allRecommendedPlugins = !startupQuiet ? await checkRecommendedPlugins().catch(() => []) : [];
391
- const pluginServiceNames = new Set(services.filter(s => s._isPlugin).map(s => s.name.toLowerCase()));
392
- const recommendedPlugins = allRecommendedPlugins.filter(p => !pluginServiceNames.has(p.name.toLowerCase()));
393
-
394
369
  if (!startupQuiet) {
395
370
  this.#welcomeComponent = new WelcomeComponent(
396
371
  this.#version,
397
372
  welcomeResult.model,
398
373
  services,
399
374
  this.#initialUpdateStatus,
400
- recommendedPlugins,
375
+ [],
376
+ plugins,
401
377
  );
402
378
 
403
379
  // Setup UI layout