@f5xc-salesdemos/xcsh 19.14.3 → 19.15.1

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.14.3",
4
+ "version": "19.15.1",
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.14.3",
54
- "@f5xc-salesdemos/pi-agent-core": "19.14.3",
55
- "@f5xc-salesdemos/pi-ai": "19.14.3",
56
- "@f5xc-salesdemos/pi-natives": "19.14.3",
57
- "@f5xc-salesdemos/pi-tui": "19.14.3",
58
- "@f5xc-salesdemos/pi-utils": "19.14.3",
53
+ "@f5xc-salesdemos/xcsh-stats": "19.15.1",
54
+ "@f5xc-salesdemos/pi-agent-core": "19.15.1",
55
+ "@f5xc-salesdemos/pi-ai": "19.15.1",
56
+ "@f5xc-salesdemos/pi-natives": "19.15.1",
57
+ "@f5xc-salesdemos/pi-tui": "19.15.1",
58
+ "@f5xc-salesdemos/pi-utils": "19.15.1",
59
59
  "@sinclair/typebox": "^0.34",
60
60
  "@xterm/headless": "^6.0",
61
61
  "ajv": "^8.20",
@@ -15,6 +15,8 @@ import { loadCapability } from "../../discovery";
15
15
  import { getExtensionNameFromPath } from "../../discovery/helpers";
16
16
  import type { ExecOptions } from "../../exec/exec";
17
17
  import { execCommand } from "../../exec/exec";
18
+ import type { ProfileCollector } from "../../internal-urls/profile-collectors";
19
+ import { registerProfileCollector as registerProfileCollectorCore } from "../../internal-urls/profile-collectors";
18
20
  import type { CustomMessage } from "../../session/messages";
19
21
  import { EventBus } from "../../utils/event-bus";
20
22
  import { getAllPluginExtensionPaths } from "../plugins/loader";
@@ -181,6 +183,10 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
181
183
  this.extension.serviceStatuses.set(contribution.name, contribution);
182
184
  }
183
185
 
186
+ registerProfileCollector(collector: ProfileCollector): void {
187
+ registerProfileCollectorCore(collector);
188
+ }
189
+
184
190
  getFlag(name: string): boolean | string | undefined {
185
191
  if (!this.extension.flags.has(name)) return undefined;
186
192
  return this.runtime.flagValues.get(name);
@@ -35,6 +35,7 @@ import type { ModelRegistry } from "../../config/model-registry";
35
35
  import type { EditToolDetails } from "../../edit";
36
36
  import type { BashResult } from "../../exec/bash-executor";
37
37
  import type { ExecOptions, ExecResult } from "../../exec/exec";
38
+ import type { ProfileCollector } from "../../internal-urls/profile-collectors";
38
39
  import type { PythonResult } from "../../ipy/executor";
39
40
  import type { Theme } from "../../modes/theme/theme";
40
41
  import type { CompactionPreparation, CompactionResult } from "../../session/compaction";
@@ -1081,6 +1082,9 @@ export interface ExtensionAPI {
1081
1082
  /** Register a service status check for the welcome screen. */
1082
1083
  registerServiceStatus(contribution: ServiceStatusContribution): void;
1083
1084
 
1085
+ /** Register a profile collector that pushes discovered person-data to the user profile. */
1086
+ registerProfileCollector(collector: ProfileCollector): void;
1087
+
1084
1088
  // =========================================================================
1085
1089
  // Actions
1086
1090
  // =========================================================================
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "19.14.3",
21
- "commit": "babc47593b34b9bdd2eafbe29e590dcd6b5beb0f",
22
- "shortCommit": "babc475",
20
+ "version": "19.15.1",
21
+ "commit": "c973489d7e76f41327ba992e44617e173cc702e1",
22
+ "shortCommit": "c973489",
23
23
  "branch": "main",
24
- "tag": "v19.14.3",
25
- "commitDate": "2026-06-08T00:40:23Z",
26
- "buildDate": "2026-06-08T01:05:25.613Z",
24
+ "tag": "v19.15.1",
25
+ "commitDate": "2026-06-08T02:49:22Z",
26
+ "buildDate": "2026-06-08T03:14:05.699Z",
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/babc47593b34b9bdd2eafbe29e590dcd6b5beb0f",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.14.3"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/c973489d7e76f41327ba992e44617e173cc702e1",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.15.1"
33
33
  };
@@ -52,4 +52,14 @@ const systemCollector: ProfileCollector = {
52
52
  // Registry
53
53
  // ---------------------------------------------------------------------------
54
54
 
55
- export const PROFILE_COLLECTORS: readonly ProfileCollector[] = [systemCollector];
55
+ const _collectors: ProfileCollector[] = [systemCollector];
56
+
57
+ export const PROFILE_COLLECTORS: readonly ProfileCollector[] = _collectors;
58
+
59
+ export function registerProfileCollector(collector: ProfileCollector): void {
60
+ if (_collectors.some(c => c.id === collector.id)) {
61
+ logger.warn(`Profile collector '${collector.id}' already registered, skipping`);
62
+ return;
63
+ }
64
+ _collectors.push(collector);
65
+ }
@@ -67,6 +67,8 @@ export interface UserProfile {
67
67
  observations?: UserProfileObservation[];
68
68
  sources?: { github?: string; system?: string; conversation?: string };
69
69
  updatedAt?: string;
70
+ /** Tracks which collector ID authoritatively owns each top-level field. */
71
+ _fieldOwnership?: Record<string, string>;
70
72
  }
71
73
 
72
74
  const PROFILE_PATH = path.join(os.homedir(), ".xcsh", "user-profile.json");
@@ -142,6 +144,57 @@ export async function seedProfile(): Promise<UserProfile> {
142
144
  return profile;
143
145
  }
144
146
 
147
+ const META_FIELDS = new Set(["sources", "observations", "updatedAt", "_fieldOwnership"]);
148
+ const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
149
+
150
+ export function reconcileProfile(target: UserProfile, source: Partial<UserProfile>, sourceId: string): void {
151
+ const ownership = target._fieldOwnership ?? {};
152
+
153
+ for (const [key, value] of Object.entries(source)) {
154
+ if (value === undefined || value === null) continue;
155
+ if (FORBIDDEN_KEYS.has(key)) continue;
156
+ const k = key as keyof UserProfile;
157
+ if (META_FIELDS.has(k)) continue;
158
+
159
+ const currentOwner = ownership[k];
160
+
161
+ if (currentOwner === "user") continue;
162
+ if (currentOwner && currentOwner !== sourceId) continue;
163
+ if (!currentOwner && target[k] !== undefined && target[k] !== null) continue;
164
+
165
+ Object.defineProperty(target, k, { value, writable: true, enumerable: true, configurable: true });
166
+ if (!currentOwner) {
167
+ Object.defineProperty(ownership, k, { value: sourceId, writable: true, enumerable: true, configurable: true });
168
+ }
169
+ }
170
+
171
+ target._fieldOwnership = ownership;
172
+ }
173
+
174
+ export async function reconcileFromCollectors(): Promise<UserProfile> {
175
+ const profile = await loadProfile();
176
+ if (!profile.sources) profile.sources = {};
177
+
178
+ for (const collector of PROFILE_COLLECTORS) {
179
+ try {
180
+ const isAvailable = await collector.available();
181
+ if (!isAvailable) {
182
+ logger.debug(`Profile collector '${collector.id}' not available, skipping`);
183
+ continue;
184
+ }
185
+ const partial = await collector.collect();
186
+ reconcileProfile(profile, partial, collector.id);
187
+ (profile.sources as Record<string, string>)[collector.id] = new Date().toISOString();
188
+ logger.debug(`Profile collector '${collector.id}' reconciled`);
189
+ } catch (err: unknown) {
190
+ logger.debug(`Profile collector '${collector.id}' failed`, { error: err });
191
+ }
192
+ }
193
+
194
+ await saveProfile(profile);
195
+ return profile;
196
+ }
197
+
145
198
  function formatAddress(addr: NonNullable<UserProfile["address"]>): string {
146
199
  const parts: string[] = [];
147
200
  if (addr.streetAddress) parts.push(addr.streetAddress);
@@ -29,7 +29,7 @@ import type { CompactOptions } from "../extensibility/extensions/types";
29
29
  import { BUILTIN_SLASH_COMMANDS, loadSlashCommands } from "../extensibility/slash-commands";
30
30
  import { resolveLocalUrlToPath } from "../internal-urls";
31
31
  import { seedComputerProfile } from "../internal-urls/computer-profile";
32
- import { seedProfile } from "../internal-urls/user-profile";
32
+ import { reconcileFromCollectors } from "../internal-urls/user-profile";
33
33
  import { renameApprovedPlanFile } from "../plan-mode/approved-plan";
34
34
  import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
35
35
  import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
@@ -321,7 +321,7 @@ export class InteractiveMode implements InteractiveModeContext {
321
321
  );
322
322
 
323
323
  // Refresh user profile in background — fire and forget
324
- seedProfile().catch(err => logger.warn("Background profile refresh failed", { error: String(err) }));
324
+ reconcileFromCollectors().catch(err => logger.warn("Background profile refresh failed", { error: String(err) }));
325
325
  // Refresh computer profile in background — fire and forget
326
326
  seedComputerProfile().catch(err =>
327
327
  logger.warn("Background computer profile refresh failed", { error: String(err) }),
@@ -55,3 +55,25 @@ For HTTPS: replace `"http": {"port": 80}` with `"https_auto_cert": {"http_redire
55
55
  - `policers` / "policer": `{"metadata":{"name":"<n>","namespace":"<ns>"},"spec":{"burst_size":<int>,"committed_information_rate":<int>}}` — network-level byte/Mbps limiting
56
56
  - `rate_limiters` / "rate limiter": `{"metadata":{"name":"<n>","namespace":"<ns>"},"spec":{"burst_size":<int>,"committed_information_rate":<int>}}` — HTTP request-level limiting (rps)
57
57
  - `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>"}}}]}}`
58
+
59
+ **SecureMesh Site v2 (`securemesh_site_v2s`) — system namespace only** — POST to `/api/config/namespaces/system/securemesh_site_v2s`. Base payload with all 12 oneOf groups set to defaults:
60
+
61
+ ```json
62
+ {"metadata":{"name":"<n>","namespace":"system"},"spec":{"azure":{"not_managed":{"node_list":[]}},"disable_ha":{},"block_all_services":{},"no_network_policy":{},"no_forward_proxy":{},"f5_proxy":{},"no_proxy_bypass":{},"logs_streaming_disabled":{},"no_s2s_connectivity_sli":{},"no_s2s_connectivity_slo":{},"disable_url_categorization":{},"disable_management_network":{}}}
63
+ ```
64
+
65
+ Each of the 12 oneOf groups has two or three mutually exclusive choices — pick exactly one per group:
66
+
67
+ |Group|Options (pick one)|Notes|
68
+ |---|---|---|
69
+ |node_ha|`"disable_ha":{}` OR `"enable_ha":{}`||
70
+ |blocked_services|`"block_all_services":{}` OR `"blocked_services":{"service_list":[{"service":"HTTP"}]}`||
71
+ |network_policy|`"no_network_policy":{}` OR `"active_enhanced_firewall_policies":{"active_enhanced_firewall_policies":[{"name":"<n>","namespace":"<ns>"}]}`|prereq: create `enhanced_firewall_policys` with `spec:{}` first|
72
+ |forward_proxy|`"no_forward_proxy":{}` OR `"active_forward_proxy_policies":{"active_forward_proxy_policies":[{"name":"<n>","namespace":"<ns>"}]}`|prereq: create `forward_proxy_policys` with `spec:{"drp_http_connect":{},"allow_all":{}}` — drp_http_connect **REQUIRED**|
73
+ |enterprise_proxy|`"f5_proxy":{}` OR `"custom_proxy":{"http_proxy":"http://proxy:8080","https_proxy":"http://proxy:8080"}`||
74
+ |proxy_bypass|`"no_proxy_bypass":{}` OR `"custom_proxy_bypass":{"bypass_list":["10.0.0.0/8"]}`||
75
+ |logs_receiver|`"logs_streaming_disabled":{}` OR `"log_receiver":{"name":"<n>","namespace":"<ns>"}`|prereq: create `global_log_receivers` with `spec:{"request_logs":{},"http_receiver":{"uri":"http://logs:8080","no_tls":{},"disable_authentication":{}}}`|
76
+ |s2s_sli|`"no_s2s_connectivity_sli":{}` OR `"dc_cluster_group_sli":{"name":"<n>","namespace":"system"}`|prereq: create `dc_cluster_groups` in system ns with `spec:{}`|
77
+ |s2s_slo|`"no_s2s_connectivity_slo":{}` OR `"dc_cluster_group_slo":{"name":"<n>","namespace":"system"}` OR `"site_mesh_group_on_slo":{"name":"<n>","namespace":"system"}`|dc_cluster_group prereq same as above; site_mesh_group prereq: `spec:{"type":"SITE_MESH_GROUP_TYPE_FULL_MESH","tunnel_type":"SITE_TO_SITE_TUNNEL_IPSEC","full_mesh":{"data_plane_mesh":{}},"bfd_disabled":{}}` — bfd_disabled **REQUIRED**|
78
+ |url_categorization|`"disable_url_categorization":{}` OR `"enable_url_categorization":{}`||
79
+ |management_network|`"disable_management_network":{}` OR `"enable_management_network":{}`||
@@ -21,7 +21,7 @@ export interface OutputBlockOptions {
21
21
  sections?: Array<{ label?: string; lines: string[] }>;
22
22
  width: number;
23
23
  applyBg?: boolean;
24
- /** Override the state-derived border color. Use sparingly only for branded core tools. */
24
+ /** Override the state-derived border color. Always takes precedence, including on error. Use for branded core tools. */
25
25
  borderColor?: ThemeColor;
26
26
  }
27
27
 
@@ -39,14 +39,11 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
39
39
  const v = theme.boxSharp.vertical;
40
40
  const cap = h.repeat(3);
41
41
  const lineWidth = Math.max(0, width);
42
- // Border colors: error→error (red), warning→warning, running/pending→spinnerAccent, success→dim.
43
- // borderColorOverride (from options) takes precedence for non-error states on F5-branded tools (e.g. XC-API);
44
- // override is always cleared on error so all tools show a consistent error border; use F5_TOOL_BORDER_COLOR.
42
+ // borderColorOverride (F5 brand chrome) always takes precedence;
43
+ // built-in tools without an override use dim borders regardless of error state.
45
44
  const resolvedBorderColor: ThemeColor =
46
- state === "error"
47
- ? "error"
48
- : (borderColorOverride ??
49
- (state === "warning" ? "warning" : state === "running" || state === "pending" ? "spinnerAccent" : "dim"));
45
+ borderColorOverride ??
46
+ (state === "warning" ? "warning" : state === "running" || state === "pending" ? "spinnerAccent" : "dim");
50
47
  const border = (text: string) => theme.fg(resolvedBorderColor, text);
51
48
  const bgFn = (() => {
52
49
  if (!state || !applyBg) return undefined;