@f5xc-salesdemos/xcsh 18.87.3 → 18.89.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +7 -7
  3. package/src/config/settings-schema.ts +0 -11
  4. package/src/extensibility/extensions/loader.ts +6 -0
  5. package/src/extensibility/extensions/runner.ts +12 -0
  6. package/src/extensibility/extensions/types.ts +17 -0
  7. package/src/internal-urls/build-info-runtime.ts +0 -1
  8. package/src/internal-urls/build-info.generated.ts +8 -8
  9. package/src/internal-urls/index.ts +0 -1
  10. package/src/internal-urls/profile-collectors.ts +1 -104
  11. package/src/internal-urls/user-profile.ts +7 -11
  12. package/src/internal-urls/xcsh-protocol.ts +4 -26
  13. package/src/modes/components/welcome-checks.ts +0 -121
  14. package/src/modes/interactive-mode.ts +51 -23
  15. package/src/modes/rpc/rpc-mode.ts +17 -7
  16. package/src/prompts/system/system-prompt.md +0 -8
  17. package/src/sdk.ts +1 -13
  18. package/src/system-prompt.ts +0 -20
  19. package/src/tools/index.ts +0 -7
  20. package/src/tools/renderers.ts +0 -5
  21. package/src/internal-urls/salesforce-context.ts +0 -745
  22. package/src/pipeline-report/benchmark.ts +0 -405
  23. package/src/pipeline-report/generator.ts +0 -684
  24. package/src/pipeline-report/index.ts +0 -3
  25. package/src/pipeline-report/renderer.ts +0 -306
  26. package/src/pipeline-report/types.ts +0 -166
  27. package/src/prompts/tools/sf-org-display.md +0 -7
  28. package/src/prompts/tools/sf-pipeline-report.md +0 -25
  29. package/src/prompts/tools/sf-query.md +0 -122
  30. package/src/prompts/tools/sf-setup.md +0 -10
  31. package/src/tools/sf/exec.ts +0 -104
  32. package/src/tools/sf/formatters.ts +0 -150
  33. package/src/tools/sf/types.ts +0 -40
  34. package/src/tools/sf-pipeline-report.ts +0 -175
  35. package/src/tools/sf-renderer.ts +0 -332
  36. package/src/tools/sf.ts +0 -391
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.88.0] - 2026-05-31
6
+
7
+ ### Changed
8
+
9
+ - Salesforce tools extracted to marketplace plugin: sf_setup, sf_query, sf_org_display, and sf_pipeline_report are now available as an installable plugin (`@f5xc-salesdemos/xcsh-salesforce`) via the Extension API instead of built-in tools. Install with `xcsh plugin install salesforce`. Context discovery, pipeline reporting, and container-adapted authentication are preserved with full feature parity. ([#1059](https://github.com/f5xc-salesdemos/xcsh/issues/1059))
10
+
5
11
  ## [18.75.0] - 2026-05-23
6
12
 
7
13
  ### Added
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5xc-salesdemos/xcsh",
4
- "version": "18.87.3",
4
+ "version": "18.89.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": "18.87.3",
54
- "@f5xc-salesdemos/pi-agent-core": "18.87.3",
55
- "@f5xc-salesdemos/pi-ai": "18.87.3",
56
- "@f5xc-salesdemos/pi-natives": "18.87.3",
57
- "@f5xc-salesdemos/pi-tui": "18.87.3",
58
- "@f5xc-salesdemos/pi-utils": "18.87.3",
53
+ "@f5xc-salesdemos/xcsh-stats": "18.89.0",
54
+ "@f5xc-salesdemos/pi-agent-core": "18.89.0",
55
+ "@f5xc-salesdemos/pi-ai": "18.89.0",
56
+ "@f5xc-salesdemos/pi-natives": "18.89.0",
57
+ "@f5xc-salesdemos/pi-tui": "18.89.0",
58
+ "@f5xc-salesdemos/pi-utils": "18.89.0",
59
59
  "@sinclair/typebox": "^0.34",
60
60
  "@xterm/headless": "^6.0",
61
61
  "ajv": "^8.18",
@@ -1360,17 +1360,6 @@ export const SETTINGS_SCHEMA = {
1360
1360
  },
1361
1361
  },
1362
1362
 
1363
- "salesforce.enabled": {
1364
- type: "boolean",
1365
- default: true,
1366
- ui: {
1367
- tab: "tools",
1368
- label: "Salesforce CLI",
1369
- description:
1370
- "Enable sf_* tools for Salesforce org management, SOQL queries, and pipeline reporting via sf CLI",
1371
- },
1372
- },
1373
-
1374
1363
  "web_search.enabled": {
1375
1364
  type: "boolean",
1376
1365
  default: true,
@@ -28,6 +28,7 @@ import type {
28
28
  LoadExtensionsResult,
29
29
  MessageRenderer,
30
30
  RegisteredCommand,
31
+ ServiceStatusContribution,
31
32
  ToolDefinition,
32
33
  } from "./types";
33
34
 
@@ -176,6 +177,10 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
176
177
  this.extension.messageRenderers.set(customType, renderer as MessageRenderer);
177
178
  }
178
179
 
180
+ registerServiceStatus(contribution: ServiceStatusContribution): void {
181
+ this.extension.serviceStatuses.set(contribution.name, contribution);
182
+ }
183
+
179
184
  getFlag(name: string): boolean | string | undefined {
180
185
  if (!this.extension.flags.has(name)) return undefined;
181
186
  return this.runtime.flagValues.get(name);
@@ -257,6 +262,7 @@ function createExtension(extensionPath: string, resolvedPath: string): Extension
257
262
  commands: new Map(),
258
263
  flags: new Map(),
259
264
  shortcuts: new Map(),
265
+ serviceStatuses: new Map(),
260
266
  };
261
267
  }
262
268
 
@@ -36,6 +36,7 @@ import type {
36
36
  RegisteredTool,
37
37
  ResourcesDiscoverEvent,
38
38
  ResourcesDiscoverResult,
39
+ ServiceStatusContribution,
39
40
  SessionBeforeBranchResult,
40
41
  SessionBeforeCompactResult,
41
42
  SessionBeforeSwitchResult,
@@ -251,6 +252,17 @@ export class ExtensionRunner {
251
252
  return tools;
252
253
  }
253
254
 
255
+ /** Get all registered service status contributions from all extensions. */
256
+ getAllRegisteredServiceStatuses(): ServiceStatusContribution[] {
257
+ const statuses: ServiceStatusContribution[] = [];
258
+ for (const ext of this.extensions) {
259
+ for (const contribution of ext.serviceStatuses.values()) {
260
+ statuses.push(contribution);
261
+ }
262
+ }
263
+ return statuses;
264
+ }
265
+
254
266
  getFlags(): Map<string, ExtensionFlag> {
255
267
  const allFlags = new Map<string, ExtensionFlag>();
256
268
  for (const ext of this.extensions) {
@@ -933,6 +933,19 @@ export interface RegisteredCommand {
933
933
  handler: (args: string, ctx: ExtensionCommandContext) => Promise<void>;
934
934
  }
935
935
 
936
+ // ============================================================================
937
+ // Service Status Contributions
938
+ // ============================================================================
939
+
940
+ export interface ServiceStatusContribution {
941
+ name: string;
942
+ check: () => Promise<{ state: "connected" | "unauthenticated" | "unavailable"; hint?: string }>;
943
+ fix?: {
944
+ prompt: string;
945
+ command: string[];
946
+ };
947
+ }
948
+
936
949
  // ============================================================================
937
950
  // Extension API
938
951
  // ============================================================================
@@ -1064,6 +1077,9 @@ export interface ExtensionAPI {
1064
1077
  /** Register a custom renderer for CustomMessageEntry. */
1065
1078
  registerMessageRenderer<T = unknown>(customType: string, renderer: MessageRenderer<T>): void;
1066
1079
 
1080
+ /** Register a service status check for the welcome screen. */
1081
+ registerServiceStatus(contribution: ServiceStatusContribution): void;
1082
+
1067
1083
  // =========================================================================
1068
1084
  // Actions
1069
1085
  // =========================================================================
@@ -1351,6 +1367,7 @@ export interface Extension {
1351
1367
  commands: Map<string, RegisteredCommand>;
1352
1368
  flags: Map<string, ExtensionFlag>;
1353
1369
  shortcuts: Map<KeyId, ExtensionShortcut>;
1370
+ serviceStatuses: Map<string, ServiceStatusContribution>;
1354
1371
  }
1355
1372
 
1356
1373
  /** Result of loading extensions. */
@@ -209,7 +209,6 @@ export function renderAboutDoc(info: RuntimeBuildInfo, context: ContextStatus |
209
209
  "SSH remote execution, image generation and analysis.",
210
210
  "",
211
211
  "SE specialization: F5 XC API integration (xcsh_api, api-catalog, api-spec),",
212
- "Salesforce pipeline intelligence (sf_query, xcsh://salesforce),",
213
212
  "F5 XC federated product docs (llms.txt hierarchy),",
214
213
  "user/computer profiling (xcsh://user, xcsh://computer),",
215
214
  "SE-specific subagents (deal-analyst, status-operator, cli-operator, github-ops).",
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "18.87.3",
21
- "commit": "13a382a1bfb96241f9e1710c1bba113f7e3ea0f5",
22
- "shortCommit": "13a382a",
20
+ "version": "18.89.0",
21
+ "commit": "e4462e8d079ea1a120c1f9253c79f78eb4d01e65",
22
+ "shortCommit": "e4462e8",
23
23
  "branch": "main",
24
- "tag": "v18.87.3",
25
- "commitDate": "2026-05-29T17:31:47Z",
26
- "buildDate": "2026-05-30T00:23:51.633Z",
24
+ "tag": "v18.89.0",
25
+ "commitDate": "2026-05-31T23:55:12Z",
26
+ "buildDate": "2026-06-01T00:31:02.396Z",
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/13a382a1bfb96241f9e1710c1bba113f7e3ea0f5",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.87.3"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/e4462e8d079ea1a120c1f9253c79f78eb4d01e65",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.89.0"
33
33
  };
@@ -36,7 +36,6 @@ export * from "./parse";
36
36
  export * from "./profile-collectors";
37
37
  export * from "./router";
38
38
  export * from "./rule-protocol";
39
- export * from "./salesforce-context";
40
39
  export * from "./skill-protocol";
41
40
  export * from "./terraform-resolve";
42
41
  export type * from "./terraform-types";
@@ -14,109 +14,6 @@ export interface ProfileCollector {
14
14
  collect(): Promise<Partial<UserProfile>>;
15
15
  }
16
16
 
17
- // ---------------------------------------------------------------------------
18
- // Salesforce
19
- // ---------------------------------------------------------------------------
20
-
21
- const salesforceCollector: ProfileCollector = {
22
- id: "salesforce",
23
- name: "Salesforce",
24
-
25
- async available(): Promise<boolean> {
26
- if (!$which("sf")) return false;
27
- try {
28
- const proc = await $`sf org display --json`.quiet().nothrow();
29
- if (proc.exitCode !== 0) return false;
30
- const parsed = JSON.parse(proc.stdout.toString()) as Record<string, unknown>;
31
- const result = parsed.result as Record<string, unknown> | undefined;
32
- return typeof result?.username === "string" && result.username.length > 0;
33
- } catch {
34
- return false;
35
- }
36
- },
37
-
38
- async collect(): Promise<Partial<UserProfile>> {
39
- try {
40
- // Get username
41
- const orgProc = await $`sf org display --json`.quiet().nothrow();
42
- if (orgProc.exitCode !== 0) return {};
43
- const orgData = JSON.parse(orgProc.stdout.toString()) as Record<string, unknown>;
44
- const orgResult = orgData.result as Record<string, unknown> | undefined;
45
- const username = orgResult?.username as string | undefined;
46
- if (!username) return {};
47
-
48
- // Build and run SOQL
49
- const soql = `SELECT Id, Username, FirstName, LastName, Email, Title, Department, Division, CompanyName, AboutMe, ManagerId, Manager.Name, Manager.Email, UserRole.Name, Profile.Name, Street, City, State, PostalCode, Country, Phone, MobilePhone FROM User WHERE Username = '${username}'`;
50
- const queryProc = await $`sf data query --query ${soql} --json`.quiet().nothrow();
51
- if (queryProc.exitCode !== 0) return {};
52
-
53
- const queryData = JSON.parse(queryProc.stdout.toString()) as Record<string, unknown>;
54
- const queryResult = queryData.result as Record<string, unknown> | undefined;
55
- const records = queryResult?.records as Record<string, unknown>[] | undefined;
56
- const rec = records?.[0];
57
- if (!rec) return {};
58
-
59
- // Map fields
60
- const profile: Partial<UserProfile> = {};
61
-
62
- if (rec.FirstName) profile.givenName = rec.FirstName as string;
63
- if (rec.LastName) profile.familyName = rec.LastName as string;
64
- if (rec.Email) profile.email = rec.Email as string;
65
-
66
- const phone = (rec.Phone || rec.MobilePhone) as string | undefined;
67
- if (phone) profile.telephone = phone;
68
-
69
- if (rec.Title) profile.jobTitle = rec.Title as string;
70
- if (rec.Department) profile.department = rec.Department as string;
71
- if (rec.Division) profile.division = rec.Division as string;
72
-
73
- const companyName = (rec.CompanyName as string) || "F5";
74
- profile.worksFor = { name: companyName };
75
-
76
- // Manager
77
- const mgr = rec.Manager as Record<string, unknown> | undefined;
78
- if (mgr) {
79
- const mgrName = mgr.Name as string | undefined;
80
- const mgrEmail = mgr.Email as string | undefined;
81
- if (mgrName || mgrEmail) {
82
- profile.manager = {};
83
- if (mgrName) {
84
- const parts = mgrName.split(" ");
85
- profile.manager.givenName = parts[0];
86
- if (parts.length > 1) profile.manager.familyName = parts.slice(1).join(" ");
87
- }
88
- if (mgrEmail) profile.manager.email = mgrEmail;
89
- }
90
- }
91
-
92
- // Address
93
- const street = rec.Street as string | undefined;
94
- const city = rec.City as string | undefined;
95
- const state = rec.State as string | undefined;
96
- const postalCode = rec.PostalCode as string | undefined;
97
- const country = rec.Country as string | undefined;
98
- if (street || city || state || postalCode || country) {
99
- profile.address = {};
100
- if (street) profile.address.streetAddress = street;
101
- if (city) profile.address.addressLocality = city;
102
- if (state) profile.address.addressRegion = state;
103
- if (postalCode) profile.address.postalCode = postalCode;
104
- if (country) profile.address.addressCountry = country;
105
- }
106
-
107
- // Identifiers
108
- if (rec.Id) {
109
- profile.identifiers = { salesforceId: rec.Id as string };
110
- }
111
-
112
- return profile;
113
- } catch (err: unknown) {
114
- logger.debug("salesforce collector failed", { error: err });
115
- return {};
116
- }
117
- },
118
- };
119
-
120
17
  // ---------------------------------------------------------------------------
121
18
  // GitHub
122
19
  // ---------------------------------------------------------------------------
@@ -225,4 +122,4 @@ const systemCollector: ProfileCollector = {
225
122
  // Registry
226
123
  // ---------------------------------------------------------------------------
227
124
 
228
- export const PROFILE_COLLECTORS: readonly ProfileCollector[] = [salesforceCollector, githubCollector, systemCollector];
125
+ export const PROFILE_COLLECTORS: readonly ProfileCollector[] = [githubCollector, systemCollector];
@@ -45,27 +45,27 @@ export interface UserProfile {
45
45
  description?: string;
46
46
  image?: string;
47
47
  sameAs?: string[];
48
- identifiers?: { github?: string; twitter?: string; salesforceId?: string };
49
- /** User-authored: short role label, e.g. 'SE', 'AE', 'CSM', 'SA'. Set manually; not derived from Salesforce. */
48
+ identifiers?: { github?: string; twitter?: string };
49
+ /** User-authored: short role label, e.g. 'SE', 'AE', 'CSM', 'SA'. Set manually. */
50
50
  role?: string;
51
51
  /**
52
52
  * User-authored: confirmed partner (AE/SE counterpart, CSM, etc.).
53
- * Set manually in user-profile.json. Survives Salesforce re-seeds.
53
+ * Set manually in user-profile.json.
54
54
  */
55
55
  partner?: {
56
- /** Salesforce User Id — used to scope pipeline queries */
56
+ /** Partner user ID — used to scope pipeline queries */
57
57
  id?: string;
58
58
  name: string;
59
59
  title?: string;
60
60
  /** Short role label, e.g. 'AE', 'SE', 'CSM' */
61
61
  role?: string;
62
62
  };
63
- /** User-authored: primary territory names. Exact Salesforce field values. Scopes pipeline reports. */
63
+ /** User-authored: primary territory names. Scopes pipeline reports. */
64
64
  territories?: string[];
65
65
  /** User-authored: quarterly quota target in dollars. Used for coverage ratio calculations. */
66
66
  quota?: number;
67
67
  observations?: UserProfileObservation[];
68
- sources?: { salesforce?: string; github?: string; system?: string; conversation?: string };
68
+ sources?: { github?: string; system?: string; conversation?: string };
69
69
  updatedAt?: string;
70
70
  }
71
71
 
@@ -164,9 +164,7 @@ export function renderProfileMarkdown(profile: UserProfile): string {
164
164
 
165
165
  const isEmpty = !profile.givenName && !profile.familyName && !profile.email && !profile.jobTitle;
166
166
  if (isEmpty) {
167
- sections.push(
168
- "No profile data yet. Use `xcsh://user?seed=true` to populate from Salesforce, GitHub, and system sources.\n",
169
- );
167
+ sections.push("No profile data yet. Use `xcsh://user?seed=true` to populate from GitHub and system sources.\n");
170
168
  sections.push("Profile facts can also be added progressively during conversation.\n");
171
169
  return sections.join("\n");
172
170
  }
@@ -267,7 +265,6 @@ export function renderProfileMarkdown(profile: UserProfile): string {
267
265
  if (profile.description) onlineLines.push(`- **Bio:** ${profile.description}`);
268
266
  if (profile.identifiers?.github) onlineLines.push(`- **GitHub:** ${profile.identifiers.github}`);
269
267
  if (profile.identifiers?.twitter) onlineLines.push(`- **Twitter/X:** ${profile.identifiers.twitter}`);
270
- if (profile.identifiers?.salesforceId) onlineLines.push(`- **Salesforce ID:** ${profile.identifiers.salesforceId}`);
271
268
  if (profile.sameAs && profile.sameAs.length > 0) {
272
269
  for (const link of profile.sameAs) {
273
270
  onlineLines.push(`- **Profile:** ${link}`);
@@ -293,7 +290,6 @@ export function renderProfileMarkdown(profile: UserProfile): string {
293
290
  sections.push("\n---\n");
294
291
  sections.push("**Sources:**");
295
292
  const srcLines: string[] = [];
296
- if (profile.sources.salesforce) srcLines.push(`Salesforce: ${profile.sources.salesforce}`);
297
293
  if (profile.sources.github) srcLines.push(`GitHub: ${profile.sources.github}`);
298
294
  if (profile.sources.system) srcLines.push(`System: ${profile.sources.system}`);
299
295
  if (profile.sources.conversation) srcLines.push(`Conversation: ${profile.sources.conversation}`);
@@ -21,6 +21,9 @@
21
21
  * - xcsh://terraform/{category}/{resource} - Self-contained resource doc
22
22
  * - xcsh://user - Human user profile
23
23
  * - xcsh://user?seed=true - Seed profile from sources and render
24
+ *
25
+ * Note: Salesforce context (xcsh://salesforce) has been extracted to the
26
+ * salesforce plugin. See packages/salesforce/ for the standalone implementation.
24
27
  */
25
28
  import * as path from "node:path";
26
29
  import { logger } from "@f5xc-salesdemos/pi-utils";
@@ -37,7 +40,6 @@ import type {
37
40
  import { getRuntimeBuildInfo, type RuntimeBuildInfo, renderAboutDoc } from "./build-info-runtime";
38
41
  import { loadComputerProfile, renderComputerProfileMarkdown, seedComputerProfile } from "./computer-profile";
39
42
  import { EMBEDDED_DOC_FILENAMES, EMBEDDED_DOCS } from "./docs-index.generated";
40
- import { loadSalesforceContext, renderSalesforceContextMarkdown, seedSalesforceContext } from "./salesforce-context";
41
43
  import { createTerraformResolver, type TerraformResolver } from "./terraform-resolve";
42
44
  import type { TerraformIndex } from "./terraform-types";
43
45
  import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
@@ -51,8 +53,6 @@ const BRANDING_HOST = "branding";
51
53
  const TERRAFORM_HOST = "terraform";
52
54
  const USER_ROUTE = "user";
53
55
  const COMPUTER_ROUTE = "computer";
54
- const SALESFORCE_ROUTE = "salesforce";
55
-
56
56
  const EMPTY_INDEX: ApiSpecIndex = { version: "unavailable", timestamp: "", domains: [] };
57
57
  const EMPTY_CATALOG_INDEX: ApiCatalogIndex = {
58
58
  version: "unavailable",
@@ -311,10 +311,6 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
311
311
  return this.#resolveComputerProfile(url);
312
312
  }
313
313
 
314
- if (host === SALESFORCE_ROUTE) {
315
- return this.#resolveSalesforceContext(url);
316
- }
317
-
318
314
  const pathname = url.rawPathname ?? url.pathname;
319
315
  const filename = host ? (pathname && pathname !== "/" ? host + pathname : host) : "";
320
316
 
@@ -357,22 +353,6 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
357
353
  };
358
354
  }
359
355
 
360
- async #resolveSalesforceContext(url: InternalUrl): Promise<InternalResource> {
361
- const params = new URLSearchParams(url.search);
362
- const shouldRefresh = params.get("refresh") === "true";
363
-
364
- const ctx = shouldRefresh ? await seedSalesforceContext() : await loadSalesforceContext();
365
- const content = renderSalesforceContextMarkdown(ctx);
366
-
367
- return {
368
- url: url.href,
369
- content,
370
- contentType: "text/markdown",
371
- size: Buffer.byteLength(content, "utf-8"),
372
- sourcePath: `xcsh://${SALESFORCE_ROUTE}`,
373
- };
374
- }
375
-
376
356
  async #listDocs(url: InternalUrl): Promise<InternalResource> {
377
357
  if (EMBEDDED_DOC_FILENAMES.length === 0) {
378
358
  throw new Error("No documentation files found");
@@ -387,7 +367,6 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
387
367
  const brandingEntry = `- [${BRANDING_HOST}](${SCHEME_PREFIX}${BRANDING_HOST}) — F5 XC branding and legacy name mapping (v${branding.version})`;
388
368
  const userEntry = `- [${USER_ROUTE}](${SCHEME_PREFIX}${USER_ROUTE}) — human user profile`;
389
369
  const computerEntry = `- [${COMPUTER_ROUTE}](${SCHEME_PREFIX}${COMPUTER_ROUTE}) — machine hardware and environment profile`;
390
- const salesforceEntry = `- [${SALESFORCE_ROUTE}](${SCHEME_PREFIX}${SALESFORCE_ROUTE}) — Salesforce pipeline context and team discovery`;
391
370
  const tf = loadTerraformIndex();
392
371
  const terraformEntry = `- [${TERRAFORM_HOST}/](${SCHEME_PREFIX}${TERRAFORM_HOST}/) — F5 XC Terraform provider (${Object.keys(tf.resources).length} resources, v${tf.version})`;
393
372
  const listing = [
@@ -398,10 +377,9 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
398
377
  terraformEntry,
399
378
  userEntry,
400
379
  computerEntry,
401
- salesforceEntry,
402
380
  ...EMBEDDED_DOC_FILENAMES.map(f => `- [${f}](${SCHEME_PREFIX}${f})`),
403
381
  ].join("\n");
404
- const totalCount = EMBEDDED_DOC_FILENAMES.length + 8;
382
+ const totalCount = EMBEDDED_DOC_FILENAMES.length + 7;
405
383
  const content = `# Documentation\n\n${totalCount} files available:\n\n${listing}\n`;
406
384
 
407
385
  return {
@@ -234,104 +234,6 @@ export async function checkGitLabStatus(cwd: string): Promise<WelcomeGitLabStatu
234
234
  }
235
235
  }
236
236
 
237
- export type SalesforceCheckState = "connected" | "auth_error" | "session_expired" | "not_configured";
238
-
239
- export interface WelcomeSalesforceStatus {
240
- state: SalesforceCheckState;
241
- username?: string;
242
- orgAlias?: string;
243
- instanceUrl?: string;
244
- }
245
-
246
- /** Idempotent startup check: sf installed -> org list -> default org -> display status. */
247
- export async function checkSalesforceStatus(_cwd: string): Promise<WelcomeSalesforceStatus | undefined> {
248
- try {
249
- if (!$which("sf")) return undefined;
250
-
251
- // Suppress telemetry consent nag (idempotent)
252
- await $`sf config set disable-telemetry true --global`.quiet().nothrow();
253
-
254
- // Step 1: Get org list
255
- const listResult = await $`sf org list --json`.quiet().nothrow();
256
- if (listResult.exitCode !== 0) return { state: "auth_error" };
257
-
258
- let listData: {
259
- result?: {
260
- nonScratchOrgs?: unknown[];
261
- sandboxes?: unknown[];
262
- scratchOrgs?: unknown[];
263
- devHubs?: unknown[];
264
- other?: unknown[];
265
- };
266
- };
267
- try {
268
- listData = JSON.parse(listResult.text());
269
- } catch {
270
- return { state: "auth_error" };
271
- }
272
-
273
- const r = listData.result ?? {};
274
- const seen = new Set<string>();
275
- const allRawOrgs = (
276
- [
277
- ...(r.nonScratchOrgs ?? []),
278
- ...(r.sandboxes ?? []),
279
- ...(r.scratchOrgs ?? []),
280
- ...(r.devHubs ?? []),
281
- ...(r.other ?? []),
282
- ] as Record<string, unknown>[]
283
- ).filter(org => {
284
- const id = String(org.orgId ?? org.orgid ?? "");
285
- if (!id || seen.has(id)) return false;
286
- seen.add(id);
287
- return true;
288
- });
289
-
290
- if (allRawOrgs.length === 0) return { state: "auth_error" };
291
-
292
- // Step 2: Find default org (normalize raw CLI fields)
293
- const defaultRaw = allRawOrgs.find(
294
- org =>
295
- (typeof org.defaultMarker === "string" && org.defaultMarker.includes("(U)")) ||
296
- org.isDefaultUsername === true,
297
- );
298
-
299
- if (!defaultRaw) {
300
- return { state: "not_configured", username: allRawOrgs[0]?.username as string | undefined };
301
- }
302
-
303
- const alias = (defaultRaw.alias ?? defaultRaw.username) as string;
304
-
305
- // Step 3: Display org details
306
- const displayResult = await $`sf org display --target-org ${alias} --json`.quiet().nothrow();
307
- if (displayResult.exitCode !== 0) {
308
- return { state: "session_expired", username: defaultRaw.username as string | undefined, orgAlias: alias };
309
- }
310
-
311
- let displayData: { result?: Record<string, unknown> };
312
- try {
313
- displayData = JSON.parse(displayResult.text());
314
- } catch {
315
- return { state: "session_expired", username: defaultRaw.username as string | undefined, orgAlias: alias };
316
- }
317
-
318
- const result = displayData.result;
319
- if (!result || result.connectedStatus !== "Connected") {
320
- return { state: "session_expired", username: defaultRaw.username as string | undefined, orgAlias: alias };
321
- }
322
-
323
- return {
324
- state: "connected",
325
- username: result.username as string | undefined,
326
- orgAlias: alias,
327
- instanceUrl: result.instanceUrl as string | undefined,
328
- };
329
- } catch (err) {
330
- logger.warn("Salesforce startup check failed", { error: String(err) });
331
- return { state: "auth_error" };
332
- }
333
- }
334
-
335
237
  export type GitHubCheckState = "connected" | "auth_error";
336
238
 
337
239
  export interface WelcomeGitHubStatus {
@@ -391,20 +293,6 @@ export function mapGitLabStatus(status: WelcomeGitLabStatus | undefined): Servic
391
293
  }
392
294
  }
393
295
 
394
- export function mapSalesforceStatus(status: WelcomeSalesforceStatus | undefined): ServiceStatus {
395
- if (!status) return { name: "Salesforce", state: "unavailable", hint: "not installed" };
396
- switch (status.state) {
397
- case "connected":
398
- return { name: "Salesforce", state: "connected" };
399
- case "session_expired":
400
- return { name: "Salesforce", state: "unauthenticated", hint: "session expired, run: sf org login web" };
401
- case "not_configured":
402
- return { name: "Salesforce", state: "unauthenticated", hint: "run: sf org login web --set-default" };
403
- default:
404
- return { name: "Salesforce", state: "unauthenticated", hint: "run: sf org login web" };
405
- }
406
- }
407
-
408
296
  export function mapGitHubStatus(status: WelcomeGitHubStatus | undefined): ServiceStatus {
409
297
  if (!status) return { name: "GitHub", state: "unavailable", hint: "not installed" };
410
298
  switch (status.state) {
@@ -609,7 +497,6 @@ export function getFixableServices(statuses: {
609
497
  gcloud: WelcomeGcloudStatus | undefined;
610
498
  github: WelcomeGitHubStatus | undefined;
611
499
  gitlab: WelcomeGitLabStatus | undefined;
612
- salesforce: WelcomeSalesforceStatus | undefined;
613
500
  }): FixableService[] {
614
501
  const fixable: FixableService[] = [];
615
502
 
@@ -629,14 +516,6 @@ export function getFixableServices(statuses: {
629
516
  recheck: async () => mapGitHubStatus(await checkGitHubStatus()),
630
517
  });
631
518
  }
632
- if (statuses.salesforce?.state === "session_expired") {
633
- fixable.push({
634
- name: "Salesforce",
635
- prompt: "Salesforce session expired",
636
- command: ["sf", "org", "login", "web"],
637
- recheck: async () => mapSalesforceStatus(await checkSalesforceStatus(getProjectDir())),
638
- });
639
- }
640
519
  if (statuses.azure?.state === "auth_error") {
641
520
  fixable.push({
642
521
  name: "Azure",