@f5xc-salesdemos/xcsh 18.47.0 → 18.48.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": "18.47.0",
4
+ "version": "18.48.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",
@@ -48,12 +48,12 @@
48
48
  "dependencies": {
49
49
  "@agentclientprotocol/sdk": "0.16.1",
50
50
  "@mozilla/readability": "^0.6",
51
- "@f5xc-salesdemos/xcsh-stats": "18.47.0",
52
- "@f5xc-salesdemos/pi-agent-core": "18.47.0",
53
- "@f5xc-salesdemos/pi-ai": "18.47.0",
54
- "@f5xc-salesdemos/pi-natives": "18.47.0",
55
- "@f5xc-salesdemos/pi-tui": "18.47.0",
56
- "@f5xc-salesdemos/pi-utils": "18.47.0",
51
+ "@f5xc-salesdemos/xcsh-stats": "18.48.1",
52
+ "@f5xc-salesdemos/pi-agent-core": "18.48.1",
53
+ "@f5xc-salesdemos/pi-ai": "18.48.1",
54
+ "@f5xc-salesdemos/pi-natives": "18.48.1",
55
+ "@f5xc-salesdemos/pi-tui": "18.48.1",
56
+ "@f5xc-salesdemos/pi-utils": "18.48.1",
57
57
  "@sinclair/typebox": "^0.34",
58
58
  "@xterm/headless": "^6.0",
59
59
  "ajv": "^8.18",
@@ -385,8 +385,8 @@ export function createInitExperimentTool(
385
385
  };
386
386
  }
387
387
 
388
- function renderInitCall(name: string, theme: Theme, width?: number): string {
389
- return `${theme.fg("toolTitle", theme.bold("init_experiment"))} ${theme.fg("contentAccent", truncateToWidth(replaceTabs(name), Math.max(20, (width ?? 100) - 20)))}`;
388
+ function renderInitCall(name: string | undefined, theme: Theme, width?: number): string {
389
+ return `${theme.fg("toolTitle", theme.bold("init_experiment"))} ${theme.fg("contentAccent", truncateToWidth(replaceTabs(name ?? ""), Math.max(20, (width ?? 100) - 20)))}`;
390
390
  }
391
391
 
392
392
  function collectLoggedRunNumbers(results: ExperimentState["results"]): Set<number> {
@@ -362,7 +362,7 @@ export function createLogExperimentTool(
362
362
  const color = args.status === "keep" ? "success" : args.status === "discard" ? "warning" : "error";
363
363
  return {
364
364
  render(width: number): string[] {
365
- const description = truncateToWidth(replaceTabs(args.description), Math.max(20, width - 30));
365
+ const description = truncateToWidth(replaceTabs(args.description ?? ""), Math.max(20, width - 30));
366
366
  return [
367
367
  `${theme.fg("toolTitle", theme.bold("log_experiment"))} ${theme.fg(color, args.status)} ${theme.fg("muted", description)}`,
368
368
  ];
@@ -773,7 +773,7 @@ function truncateAsiValue(value: ASIData[string]): string {
773
773
  function renderSummary(details: LogDetails, theme: Theme, width?: number): string {
774
774
  const { experiment, state } = details;
775
775
  const color = experiment.status === "keep" ? "success" : experiment.status === "discard" ? "warning" : "error";
776
- let summary = `${theme.fg(color, experiment.status.toUpperCase())} ${theme.fg("muted", truncateToWidth(replaceTabs(experiment.description), Math.max(20, (width ?? 100) - 30)))}`;
776
+ let summary = `${theme.fg(color, experiment.status.toUpperCase())} ${theme.fg("muted", truncateToWidth(replaceTabs(experiment.description ?? ""), Math.max(20, (width ?? 100) - 30)))}`;
777
777
  summary += ` ${theme.fg("contentAccent", `${state.metricName}=${formatNum(experiment.metric, state.metricUnit)}`)}`;
778
778
  if (state.bestMetric !== null) {
779
779
  summary += ` ${theme.fg("dim", `baseline ${formatNum(state.bestMetric, state.metricUnit)}`)}`;
@@ -383,7 +383,7 @@ export function createRunExperimentTool(
383
383
  renderCall(args, _options, theme): Component {
384
384
  return {
385
385
  render(width: number): string[] {
386
- const commandPreview = truncateToWidth(replaceTabs(args.command), Math.max(20, width - 20));
386
+ const commandPreview = truncateToWidth(replaceTabs(args.command ?? ""), Math.max(20, width - 20));
387
387
  return [`${theme.fg("toolTitle", theme.bold("run_experiment"))} ${theme.fg("muted", commandPreview)}`];
388
388
  },
389
389
  invalidate() {},
@@ -1622,15 +1622,65 @@ export const SETTINGS_SCHEMA = {
1622
1622
  ui: { tab: "tasks", label: "Skill Commands", description: "Register skills as /skill:name commands" },
1623
1623
  },
1624
1624
 
1625
- "skills.enableCodexUser": { type: "boolean", default: true },
1625
+ "skills.enableCodexUser": {
1626
+ type: "boolean",
1627
+ default: false,
1628
+ ui: {
1629
+ tab: "tasks",
1630
+ label: "Codex User Skills",
1631
+ description: "Load skills from Codex configuration",
1632
+ },
1633
+ },
1626
1634
 
1627
- "skills.enableClaudeUser": { type: "boolean", default: true },
1635
+ "skills.enableClaudeUser": {
1636
+ type: "boolean",
1637
+ default: false,
1638
+ ui: {
1639
+ tab: "tasks",
1640
+ label: "Claude User Skills",
1641
+ description: "Load skills from ~/.claude/skills/",
1642
+ },
1643
+ },
1628
1644
 
1629
- "skills.enableClaudeProject": { type: "boolean", default: true },
1645
+ "skills.enableClaudeProject": {
1646
+ type: "boolean",
1647
+ default: false,
1648
+ ui: {
1649
+ tab: "tasks",
1650
+ label: "Claude Project Skills",
1651
+ description: "Load skills from .claude/skills/",
1652
+ },
1653
+ },
1630
1654
 
1631
- "skills.enablePiUser": { type: "boolean", default: true },
1655
+ "skills.enableClaudePlugins": {
1656
+ type: "boolean",
1657
+ default: false,
1658
+ ui: {
1659
+ tab: "tasks",
1660
+ label: "Claude Marketplace Skills",
1661
+ description: "Load skills from Claude Code marketplace plugins (~/.claude/plugins/cache/)",
1662
+ },
1663
+ },
1664
+
1665
+ "skills.enablePiUser": {
1666
+ type: "boolean",
1667
+ default: true,
1668
+ ui: {
1669
+ tab: "tasks",
1670
+ label: "xcsh User Skills",
1671
+ description: "Load user-level skills from ~/.xcsh/agent/skills/",
1672
+ },
1673
+ },
1632
1674
 
1633
- "skills.enablePiProject": { type: "boolean", default: true },
1675
+ "skills.enablePiProject": {
1676
+ type: "boolean",
1677
+ default: true,
1678
+ ui: {
1679
+ tab: "tasks",
1680
+ label: "xcsh Project Skills",
1681
+ description: "Load project-level skills from .xcsh/skills/",
1682
+ },
1683
+ },
1634
1684
 
1635
1685
  "skills.customDirectories": { type: "array", default: [] as string[] },
1636
1686
 
@@ -82,6 +82,7 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
82
82
  enableCodexUser = true,
83
83
  enableClaudeUser = true,
84
84
  enableClaudeProject = true,
85
+ enableClaudePlugins = false,
85
86
  enablePiUser = true,
86
87
  enablePiProject = true,
87
88
  customDirectories = [],
@@ -105,7 +106,7 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
105
106
  if (provider === "claude" && level === "project") return enableClaudeProject;
106
107
  if (provider === "native" && level === "user") return enablePiUser;
107
108
  if (provider === "native" && level === "project") return enablePiProject;
108
- // For other providers (agents, claude-plugins, etc.), treat them as built-in skill sources.
109
+ if (provider === "claude-plugins") return enableClaudePlugins;
109
110
  return anyBuiltInSkillSourceEnabled;
110
111
  }
111
112
 
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "18.47.0",
21
- "commit": "d0994b94529ed5464212568134c7dc9d75506b21",
22
- "shortCommit": "d0994b9",
20
+ "version": "18.48.1",
21
+ "commit": "158e9c98808fdbaeac9b8c2e0623768abffa800e",
22
+ "shortCommit": "158e9c9",
23
23
  "branch": "main",
24
- "tag": "v18.47.0",
25
- "commitDate": "2026-05-06T06:04:50Z",
26
- "buildDate": "2026-05-06T06:30:34.654Z",
27
- "dirty": false,
24
+ "tag": "v18.48.1",
25
+ "commitDate": "2026-05-06T15:22:57Z",
26
+ "buildDate": "2026-05-06T15:49:54.715Z",
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/d0994b94529ed5464212568134c7dc9d75506b21",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.47.0"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/158e9c98808fdbaeac9b8c2e0623768abffa800e",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.48.1"
33
33
  };
@@ -2,6 +2,7 @@ import type { Model } from "@f5xc-salesdemos/pi-ai";
2
2
  import { validateApiKeyAgainstModelsEndpoint } from "@f5xc-salesdemos/pi-ai/utils/oauth/api-key-validation";
3
3
  import { $which, logger } from "@f5xc-salesdemos/pi-utils";
4
4
  import { $ } from "bun";
5
+ import { loadProfile } from "../../internal-urls/user-profile";
5
6
  import { type AuthStatus, ContextService } from "../../services/f5xc-context";
6
7
  import { deriveTenantFromUrl } from "../../services/f5xc-env";
7
8
  import type { AuthStorage } from "../../session/auth-storage";
@@ -426,3 +427,99 @@ export function mapAzureStatus(status: WelcomeAzureStatus | undefined): ServiceS
426
427
  return { name: "Azure", state: "unauthenticated", hint: "run: az login --use-device-code" };
427
428
  }
428
429
  }
430
+
431
+ export type AwsCheckState = "connected" | "auth_error";
432
+
433
+ export interface WelcomeAwsStatus {
434
+ state: AwsCheckState;
435
+ }
436
+
437
+ export async function checkAwsStatus(): Promise<WelcomeAwsStatus | undefined> {
438
+ try {
439
+ if (!$which("aws")) return undefined;
440
+ const result = await $`aws sts get-caller-identity --output json`.quiet().nothrow();
441
+ return { state: result.exitCode === 0 ? "connected" : "auth_error" };
442
+ } catch (err) {
443
+ logger.warn("AWS startup check failed", { error: String(err) });
444
+ return { state: "auth_error" };
445
+ }
446
+ }
447
+
448
+ export function mapAwsStatus(status: WelcomeAwsStatus | undefined): ServiceStatus {
449
+ if (!status) return { name: "AWS", state: "unavailable", hint: "not installed" };
450
+ switch (status.state) {
451
+ case "connected":
452
+ return { name: "AWS", state: "connected" };
453
+ case "auth_error":
454
+ return { name: "AWS", state: "unauthenticated", hint: "run: aws configure" };
455
+ }
456
+ }
457
+
458
+ export type GcloudCheckState = "connected" | "auth_error";
459
+
460
+ export interface WelcomeGcloudStatus {
461
+ state: GcloudCheckState;
462
+ }
463
+
464
+ export async function checkGcloudStatus(): Promise<WelcomeGcloudStatus | undefined> {
465
+ try {
466
+ if (!$which("gcloud")) return undefined;
467
+ const result = await $`gcloud auth list --format=value(account)`.quiet().nothrow();
468
+ const hasAccount = result.text().trim().length > 0;
469
+ return { state: hasAccount ? "connected" : "auth_error" };
470
+ } catch (err) {
471
+ logger.warn("Google Cloud startup check failed", { error: String(err) });
472
+ return { state: "auth_error" };
473
+ }
474
+ }
475
+
476
+ export function mapGcloudStatus(status: WelcomeGcloudStatus | undefined): ServiceStatus {
477
+ if (!status) return { name: "Google Cloud", state: "unavailable", hint: "not installed" };
478
+ switch (status.state) {
479
+ case "connected":
480
+ return { name: "Google Cloud", state: "connected" };
481
+ case "auth_error":
482
+ return { name: "Google Cloud", state: "unauthenticated", hint: "run: gcloud auth login" };
483
+ }
484
+ }
485
+
486
+ export type ProfileCheckState = "current" | "stale" | "missing";
487
+
488
+ export interface WelcomeProfileStatus {
489
+ state: ProfileCheckState;
490
+ name?: string;
491
+ updatedAt?: string;
492
+ staleDays?: number;
493
+ }
494
+
495
+ const PROFILE_STALE_HOURS = 24;
496
+
497
+ /** Check user profile freshness. Returns undefined only on unexpected error. */
498
+ export async function checkProfileStatus(): Promise<WelcomeProfileStatus | undefined> {
499
+ try {
500
+ const profile = await loadProfile();
501
+ if (!profile.givenName && !profile.familyName) {
502
+ return { state: "missing" };
503
+ }
504
+ const name = [profile.givenName, profile.familyName].filter(Boolean).join(" ");
505
+ const updatedAt = profile.updatedAt;
506
+
507
+ if (!updatedAt) {
508
+ return { state: "stale", name };
509
+ }
510
+
511
+ const ageHours = (Date.now() - new Date(updatedAt).getTime()) / (1000 * 60 * 60);
512
+ if (ageHours > PROFILE_STALE_HOURS) {
513
+ return {
514
+ state: "stale",
515
+ name,
516
+ updatedAt,
517
+ staleDays: Math.floor(ageHours / 24),
518
+ };
519
+ }
520
+
521
+ return { state: "current", name, updatedAt };
522
+ } catch {
523
+ return { state: "missing" };
524
+ }
525
+ }
@@ -28,6 +28,7 @@ import type {
28
28
  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
+ import { seedProfile } from "../internal-urls/user-profile";
31
32
  import { renameApprovedPlanFile } from "../plan-mode/approved-plan";
32
33
  import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
33
34
  import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
@@ -51,12 +52,16 @@ import { StatusLineComponent } from "./components/status-line";
51
52
  import type { ToolExecutionHandle } from "./components/tool-execution";
52
53
  import { type UpdateStatus, WelcomeComponent } from "./components/welcome";
53
54
  import {
55
+ checkAwsStatus,
54
56
  checkAzureStatus,
57
+ checkGcloudStatus,
55
58
  checkGitHubStatus,
56
59
  checkGitLabStatus,
57
60
  checkSalesforceStatus,
61
+ mapAwsStatus,
58
62
  mapAzureStatus,
59
63
  mapContextStatus,
64
+ mapGcloudStatus,
60
65
  mapGitHubStatus,
61
66
  mapGitLabStatus,
62
67
  mapSalesforceStatus,
@@ -318,17 +323,22 @@ export class InteractiveMode implements InteractiveModeContext {
318
323
  getProjectDir(),
319
324
  );
320
325
 
321
- // Run blocking welcome screen status checks (model + context + gitlab + github + salesforce + azure) in parallel
322
- const [welcomeResult, gitlabStatus, salesforceStatus, githubStatus, azureStatus] = await Promise.all([
323
- logger.time("InteractiveMode.init:welcomeChecks", () =>
324
- runWelcomeChecks(this.session.model, this.session.modelRegistry.authStorage),
325
- ),
326
- checkGitLabStatus(getProjectDir()).catch(() => undefined),
327
- checkSalesforceStatus(getProjectDir()).catch(() => undefined),
328
- checkGitHubStatus().catch(() => undefined),
329
- checkAzureStatus().catch(() => undefined),
330
- ]);
331
-
326
+ // Run blocking welcome screen status checks in parallel
327
+ const [welcomeResult, gitlabStatus, salesforceStatus, githubStatus, azureStatus, awsStatus, gcloudStatus] =
328
+ await Promise.all([
329
+ logger.time("InteractiveMode.init:welcomeChecks", () =>
330
+ runWelcomeChecks(this.session.model, this.session.modelRegistry.authStorage),
331
+ ),
332
+ checkGitLabStatus(getProjectDir()).catch(() => undefined),
333
+ checkSalesforceStatus(getProjectDir()).catch(() => undefined),
334
+ checkGitHubStatus().catch(() => undefined),
335
+ checkAzureStatus().catch(() => undefined),
336
+ checkAwsStatus().catch(() => undefined),
337
+ checkGcloudStatus().catch(() => undefined),
338
+ ]);
339
+
340
+ // Refresh user profile in background — fire and forget
341
+ seedProfile().catch(err => logger.warn("Background profile refresh failed", { error: String(err) }));
332
342
  const startupQuiet = settings.get("startup.quiet");
333
343
  this.#welcomeComponent = undefined;
334
344
 
@@ -348,6 +358,8 @@ export class InteractiveMode implements InteractiveModeContext {
348
358
  mapGitHubStatus(githubStatus),
349
359
  mapSalesforceStatus(salesforceStatus),
350
360
  mapAzureStatus(azureStatus),
361
+ mapAwsStatus(awsStatus),
362
+ mapGcloudStatus(gcloudStatus),
351
363
  ]
352
364
  : [];
353
365
  this.#welcomeComponent = new WelcomeComponent(
@@ -149,6 +149,17 @@ Use these values when constructing API payloads and resource names.
149
149
  Available F5 XC documentation topics: {{knowledgeTopics}}.
150
150
  {{/if}}
151
151
  {{/if}}
152
+ {{#if userProfile}}
153
+ ## Primary Human
154
+
155
+ {{userProfile.name}} ({{userProfile.role}}, {{userProfile.org}}).
156
+ Full profile at `xcsh://user`. **MUST** read when:
157
+ - Addressing the user by name or drafting communications from/to them
158
+ - A tool call needs personal identifiers (Salesforce user ID, GitHub username, email, phone)
159
+ - User asks about themselves ("my email", "who is my manager", "where am I from")
160
+ - Answering relationship/identity questions ("who is your human", "who do you work with")
161
+ **SHOULD NOT** read for routine technical work, code changes, or product questions.
162
+ {{/if}}
152
163
 
153
164
  {{#if contextFiles.length}}
154
165
  <context>
@@ -194,6 +205,8 @@ Most tools resolve custom protocol URLs to internal resources (not web URLs):
194
205
  - `xcsh://about` — Identity, version, build fingerprint, architecture, self-improvement. **MUST** read for any question about xcsh before exploring `~/.xcsh/`.
195
206
  This document contains the authoritative repository URL, issues URL, and source location.
196
207
  For identity questions (source code, repo, version, who built this) — answer from `xcsh://about` alone. Do not call external GitHub tools.
208
+ - `xcsh://user` — Primary human user profile (identity, employment, contact, demographics). Read when personal identity context is needed. Do not read proactively on every turn.
209
+ - `xcsh://user?seed=true` — Refresh profile from Salesforce, GitHub, and system sources.
197
210
  - `xcsh://api-spec/` — F5 XC API specifications (schema introspection, field types, validation).
198
211
  - `xcsh://api-catalog/` — F5 XC API operations catalog (CRUD execution).
199
212
 
package/src/sdk.ts CHANGED
@@ -72,6 +72,7 @@ import {
72
72
  RuleProtocolHandler,
73
73
  SkillProtocolHandler,
74
74
  } from "./internal-urls";
75
+ import { loadProfile } from "./internal-urls/user-profile";
75
76
  import { disposeAllKernelSessions, disposeKernelSessionsByOwner } from "./ipy/executor";
76
77
  import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "./lsp/startup-events";
77
78
  import { discoverAndLoadMCPTools, type MCPManager, type MCPToolsLoadResult } from "./mcp";
@@ -1450,6 +1451,27 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1450
1451
  }
1451
1452
  appendPrompt = parts.join("\n\n");
1452
1453
  }
1454
+ // Load compact user profile for system prompt hint
1455
+ let userProfile: { name: string; role: string; org: string } | undefined;
1456
+ try {
1457
+ const _profile = await loadProfile();
1458
+ if (_profile.givenName || _profile.familyName) {
1459
+ const _name = [_profile.givenName, _profile.familyName].filter(Boolean).join(" ");
1460
+ if (_name) {
1461
+ userProfile = {
1462
+ name: _name,
1463
+ role: _profile.jobTitle ?? "",
1464
+ org:
1465
+ typeof _profile.worksFor === "string"
1466
+ ? _profile.worksFor
1467
+ : ((_profile.worksFor as { name?: string } | undefined)?.name ?? ""),
1468
+ };
1469
+ }
1470
+ }
1471
+ } catch {
1472
+ // No profile — hint block omitted
1473
+ }
1474
+
1453
1475
  const defaultPrompt = await buildSystemPromptInternal({
1454
1476
  cwd,
1455
1477
  skills,
@@ -1467,6 +1489,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1467
1489
  eagerTasks,
1468
1490
  secretsEnabled,
1469
1491
  context: contextForPrompt,
1492
+ userProfile,
1470
1493
  knowledgeTopics,
1471
1494
  contextSkillDirs,
1472
1495
  contextIncludeSkills,
@@ -1495,6 +1518,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1495
1518
  eagerTasks,
1496
1519
  secretsEnabled,
1497
1520
  context: contextForPrompt,
1521
+ userProfile,
1498
1522
  knowledgeTopics,
1499
1523
  contextSkillDirs,
1500
1524
  contextIncludeSkills,
@@ -459,6 +459,12 @@ export interface BuildSystemPromptOptions {
459
459
  credentialSource: string;
460
460
  authStatus: string;
461
461
  };
462
+ /** Compact user profile hint injected into Workspace section. Omit when no profile exists. */
463
+ userProfile?: {
464
+ name: string;
465
+ role: string;
466
+ org: string;
467
+ };
462
468
  knowledgeTopics?: string;
463
469
  contextSkillDirs?: string[];
464
470
  contextIncludeSkills?: string[];
@@ -650,6 +656,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
650
656
  eagerTasks,
651
657
  secretsEnabled,
652
658
  context,
659
+ userProfile: options.userProfile,
653
660
  knowledgeTopics: options.knowledgeTopics,
654
661
  };
655
662
  let rendered = prompt.render(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);