@f5xc-salesdemos/xcsh 18.49.0 → 18.49.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.49.0",
4
+ "version": "18.49.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.49.0",
52
- "@f5xc-salesdemos/pi-agent-core": "18.49.0",
53
- "@f5xc-salesdemos/pi-ai": "18.49.0",
54
- "@f5xc-salesdemos/pi-natives": "18.49.0",
55
- "@f5xc-salesdemos/pi-tui": "18.49.0",
56
- "@f5xc-salesdemos/pi-utils": "18.49.0",
51
+ "@f5xc-salesdemos/xcsh-stats": "18.49.1",
52
+ "@f5xc-salesdemos/pi-agent-core": "18.49.1",
53
+ "@f5xc-salesdemos/pi-ai": "18.49.1",
54
+ "@f5xc-salesdemos/pi-natives": "18.49.1",
55
+ "@f5xc-salesdemos/pi-tui": "18.49.1",
56
+ "@f5xc-salesdemos/pi-utils": "18.49.1",
57
57
  "@sinclair/typebox": "^0.34",
58
58
  "@xterm/headless": "^6.0",
59
59
  "ajv": "^8.18",
@@ -186,7 +186,7 @@ export function renderAboutDoc(info: RuntimeBuildInfo, context: ContextStatus |
186
186
  "",
187
187
  "xcsh is a fork of [badlogic/pi-mono](https://github.com/badlogic/pi-mono).",
188
188
  "Upstream authors: Mario Zechner (badlogic) and contributors. Fork maintainer:",
189
- "Robin Mordasiewicz (f5xc-salesdemos). The fork adds F5 XC product knowledge,",
189
+ "f5xc-salesdemos. The fork adds F5 XC product knowledge,",
190
190
  "SE-specific skills, and the federated llms.txt hierarchy.",
191
191
  "",
192
192
  "## Architecture",
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "18.49.0",
21
- "commit": "4c512f45cdaf128a400c49b58f4738baf3f00b1e",
22
- "shortCommit": "4c512f4",
20
+ "version": "18.49.1",
21
+ "commit": "e8fa2d917ec4476ac2e1bb9f4141992961084755",
22
+ "shortCommit": "e8fa2d9",
23
23
  "branch": "main",
24
- "tag": "v18.49.0",
25
- "commitDate": "2026-05-07T06:54:02Z",
26
- "buildDate": "2026-05-07T13:31:43.899Z",
24
+ "tag": "v18.49.1",
25
+ "commitDate": "2026-05-08T02:56:04Z",
26
+ "buildDate": "2026-05-08T03:26:46.079Z",
27
27
  "dirty": false,
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/4c512f45cdaf128a400c49b58f4738baf3f00b1e",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.49.0"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/e8fa2d917ec4476ac2e1bb9f4141992961084755",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.49.1"
33
33
  };
@@ -72,6 +72,8 @@ export interface ComputerHint {
72
72
  model?: string;
73
73
  managed?: boolean;
74
74
  admin?: boolean;
75
+ /** Number of endpoint security agents detected (CrowdStrike, Defender, etc.) */
76
+ endpointAgentCount?: number;
75
77
  }
76
78
 
77
79
  export interface ManagementStatus {
@@ -549,13 +551,16 @@ export function buildComputerHint(profile: ComputerProfile): ComputerHint | unde
549
551
  return {
550
552
  ramGB: profile.totalMemoryGB,
551
553
  cpu: profile.cpuModel ?? "unknown",
552
- os: [profile.platform, profile.osVersion ?? profile.osRelease].filter(Boolean).join(" "),
554
+ os: [profile.platform === "darwin" ? "macOS" : profile.platform, profile.osVersion ?? profile.osRelease]
555
+ .filter(Boolean)
556
+ .join(" "),
553
557
  cores: profile.cpuLogicalCores,
554
558
  shell: profile.shell ? path.basename(profile.shell) : undefined,
555
559
  diskFree: profile.diskFree,
556
560
  model: profile.machineModel,
557
561
  managed: profile.management?.isManaged,
558
562
  admin: profile.security?.isAdmin,
563
+ endpointAgentCount: profile.endpointAgents?.length,
559
564
  };
560
565
  }
561
566
 
@@ -2,7 +2,7 @@ import * as os from "node:os";
2
2
  import * as path from "node:path";
3
3
  import { $which, isEnoent, logger } from "@f5xc-salesdemos/pi-utils";
4
4
  import { $ } from "bun";
5
- import { loadProfile } from "./user-profile";
5
+ import { loadProfile, type UserProfile } from "./user-profile";
6
6
 
7
7
  // ---------------------------------------------------------------------------
8
8
  // Types
@@ -12,8 +12,8 @@ export interface SalesforcePartner {
12
12
  id: string;
13
13
  name: string;
14
14
  title?: string;
15
- /** "AE" = Account Executive/Manager, "SE" = Solutions Engineer */
16
- role: "AE" | "SE" | "other";
15
+ /** Freeform role label. Common: 'AE', 'SE', 'CSM', 'SA'. Defaults to 'Partner' when unknown. */
16
+ role: string;
17
17
  }
18
18
 
19
19
  export interface TerritoryDetail {
@@ -50,13 +50,16 @@ export interface SalesforceContext {
50
50
 
51
51
  // Role
52
52
  roleName?: string;
53
+ /** Auto-inferred role label from UserRole.Name (e.g. 'SE', 'AE', 'CSM'). */
54
+ discoveredRole?: string;
53
55
 
54
56
  /**
55
- * Confirmed AE/SE partner. User-provided, not derived from manager chain.
56
- * Manager hierarchy is unreliable (stale data, rehire scenarios, poor hygiene).
57
+ * @deprecated Use UserProfile.partner instead. Kept for backward-compat cache reads.
58
+ * Removed from seedSalesforceContext new data goes to user-profile.json.
57
59
  */
58
60
  confirmedPartner?: SalesforcePartner;
59
-
61
+ /** Auto-discovered partner from OpportunityTeamMember co-membership. */
62
+ discoveredPartner?: SalesforcePartner;
60
63
  // Manager chain — kept for reference, unreliable for team discovery
61
64
  managerId?: string;
62
65
  managerName?: string;
@@ -69,6 +72,7 @@ export interface SalesforceContext {
69
72
  // Discovered pipeline universe
70
73
  territories?: string[];
71
74
  territoryDetails?: TerritoryDetail[];
75
+ /** @deprecated Use UserProfile.territories instead. Kept for backward-compat cache reads. */
72
76
  confirmedTerritories?: string[];
73
77
  productSegmentations?: string[];
74
78
  useCaseCategories?: string[];
@@ -115,6 +119,12 @@ export interface SalesforceHint {
115
119
  dealCount: number;
116
120
  accountCount: number;
117
121
  territories?: string;
122
+ /** Forecast breakdown: compact 'Commit $X + Best $Y + Pipe $Z' string */
123
+ forecastBreakdown?: string;
124
+ /** Partner name from user profile or auto-discovery */
125
+ partnerName?: string;
126
+ /** Partner role label, e.g. 'AE', 'SE', 'CSM' */
127
+ partnerRole?: string;
118
128
  }
119
129
 
120
130
  // ---------------------------------------------------------------------------
@@ -354,6 +364,42 @@ async function discoverTeamRoles(userId: string): Promise<Partial<SalesforceCont
354
364
  return { teamRoles: unique };
355
365
  }
356
366
 
367
+ /** Infer a short role label from a Salesforce User title. Generic — no company-specific logic. */
368
+ function inferRoleFromTitle(title: string): string {
369
+ const t = title.toLowerCase();
370
+ if (t.includes("solution") || t.includes("systems engineer") || t.includes("pre-sales") || t.includes("presales"))
371
+ return "SE";
372
+ if (t.includes("account") && (t.includes("executive") || t.includes("manager"))) return "AE";
373
+ if (t.includes("account") && t.includes("mgr")) return "AE";
374
+ if (t.includes("customer success")) return "CSM";
375
+ if (t.includes("architect")) return "SA";
376
+ if (t.includes("sales") && t.includes("engineer")) return "SE";
377
+ if (t.includes("territory") && (t.includes("manager") || t.includes("mgr"))) return "AE";
378
+ return "Partner";
379
+ }
380
+
381
+ async function discoverPartner(userId: string): Promise<Partial<SalesforceContext>> {
382
+ // Find users who appear most frequently on the same open opportunities
383
+ const records = await runSfQuery(
384
+ `SELECT UserId, User.Name, User.Title, COUNT(Id) cnt FROM OpportunityTeamMember WHERE OpportunityId IN (SELECT OpportunityId FROM OpportunityTeamMember WHERE UserId = '${userId}' AND Opportunity.IsClosed = false) AND UserId != '${userId}' GROUP BY UserId, User.Name, User.Title ORDER BY COUNT(Id) DESC LIMIT 3`,
385
+ );
386
+ if (records.length === 0) return {};
387
+
388
+ const top = records[0];
389
+ const userObj = top.User as Record<string, unknown> | undefined;
390
+ const name = (userObj?.Name ?? top.Name ?? "") as string;
391
+ const title = (userObj?.Title ?? top.Title ?? "") as string;
392
+ const id = (top.UserId ?? "") as string;
393
+ if (!name || !id) return {};
394
+
395
+ // Infer role from title
396
+ const role = inferRoleFromTitle(title);
397
+
398
+ return {
399
+ discoveredPartner: { id, name, title: title || undefined, role },
400
+ };
401
+ }
402
+
357
403
  async function discoverRoleAndTeam(userId: string): Promise<Partial<SalesforceContext>> {
358
404
  const userRecords = await runSfQuery(
359
405
  `SELECT UserRole.Name, ManagerId, Manager.Name FROM User WHERE Id = '${userId}'`,
@@ -368,6 +414,11 @@ async function discoverRoleAndTeam(userId: string): Promise<Partial<SalesforceCo
368
414
 
369
415
  const result: Partial<SalesforceContext> = { roleName, managerId, managerName };
370
416
 
417
+ // Infer user's own role from their Salesforce UserRole.Name or title
418
+ if (roleName) {
419
+ result.discoveredRole = inferRoleFromTitle(roleName);
420
+ }
421
+
371
422
  if (managerId) {
372
423
  const teamRecords = await runSfQuery(
373
424
  `SELECT Id, Name, Title FROM User WHERE ManagerId = '${managerId}' AND IsActive = true ORDER BY Name`,
@@ -389,15 +440,13 @@ async function discoverRoleAndTeam(userId: string): Promise<Partial<SalesforceCo
389
440
  export async function discoverSalesforceContext(): Promise<SalesforceContext | null> {
390
441
  if (!$which("sf")) return null;
391
442
 
392
- const orgInfo = await getOrgInfo();
443
+ const [orgInfo, customFields] = await Promise.all([getOrgInfo(), detectCustomFields()]);
393
444
  if (!orgInfo) return null;
394
445
 
395
446
  const profile = await loadProfile();
396
447
  const userId = profile.identifiers?.salesforceId;
397
448
  if (!userId) return null;
398
449
 
399
- const customFields = await detectCustomFields();
400
-
401
450
  const results = await Promise.all([
402
451
  discoverTerritories(userId, customFields).catch(() => ({})),
403
452
  discoverAccounts(userId).catch(() => ({})),
@@ -407,6 +456,7 @@ export async function discoverSalesforceContext(): Promise<SalesforceContext | n
407
456
  discoverPipelineSummary(userId).catch(() => ({})),
408
457
  discoverTeamRoles(userId).catch(() => ({})),
409
458
  discoverRoleAndTeam(userId).catch(() => ({})),
459
+ discoverPartner(userId).catch(() => ({})),
410
460
  ]);
411
461
 
412
462
  const merged: SalesforceContext = {
@@ -436,21 +486,55 @@ export async function seedSalesforceContext(): Promise<SalesforceContext | null>
436
486
  // Hint builder
437
487
  // ---------------------------------------------------------------------------
438
488
 
439
- export function buildSalesforceHint(ctx: SalesforceContext | null): SalesforceHint | undefined {
489
+ export function buildSalesforceHint(
490
+ ctx: SalesforceContext | null,
491
+ profile?: { partner?: UserProfile["partner"]; territories?: string[] },
492
+ ): SalesforceHint | undefined {
440
493
  if (!ctx?.pipelineSummary) return undefined;
441
494
  const total = ctx.pipelineSummary.total;
442
- const formatted =
443
- total >= 1_000_000
444
- ? `$${(total / 1_000_000).toFixed(1)}M`
445
- : total >= 1_000
446
- ? `$${(total / 1_000).toFixed(0)}K`
447
- : `$${total.toFixed(0)}`;
448
- const topTerritories = ctx.territories?.slice(0, 3).join(", ");
495
+ const fmtAmount = (n: number) =>
496
+ n >= 1_000_000
497
+ ? `$${(n / 1_000_000).toFixed(1)}M`
498
+ : n >= 1_000
499
+ ? `$${(n / 1_000).toFixed(0)}K`
500
+ : `$${n.toFixed(0)}`;
501
+ const formatted = fmtAmount(total);
502
+
503
+ // Territory priority: user-profile > deprecated confirmed > top 3 discovered
504
+ const territorySource = profile?.territories?.length
505
+ ? profile.territories
506
+ : ctx.confirmedTerritories?.length
507
+ ? ctx.confirmedTerritories
508
+ : ctx.territories?.slice(0, 3);
509
+ const topTerritories = territorySource?.join(", ");
510
+
511
+ // Forecast breakdown
512
+ const byForecast = ctx.pipelineSummary.byForecast;
513
+ const forecastParts: string[] = [];
514
+ for (const cat of ["Commit", "Best Case", "Pipeline"]) {
515
+ const entry = byForecast[cat];
516
+ if (entry && entry.amount > 0) {
517
+ const label = cat === "Best Case" ? "BC" : cat === "Pipeline" ? "Pipe" : cat;
518
+ forecastParts.push(`${label} ${fmtAmount(entry.amount)}`);
519
+ }
520
+ }
521
+ const forecastBreakdown = forecastParts.length > 0 ? forecastParts.join(", ") : undefined;
522
+
523
+ // Partner priority: user-profile > deprecated confirmed > auto-discovered
524
+ const profilePartner = profile?.partner;
525
+ const partner = profilePartner ?? ctx.confirmedPartner ?? ctx.discoveredPartner;
526
+ const isUserAuthored = !!profilePartner || !!ctx.confirmedPartner;
527
+ const partnerName = partner?.name ? (isUserAuthored ? partner.name : `${partner.name} (unconfirmed)`) : undefined;
528
+ const partnerRole = partner?.role;
529
+
449
530
  return {
450
531
  pipelineTotal: formatted,
451
532
  dealCount: ctx.pipelineSummary.dealCount,
452
533
  accountCount: ctx.activeAccounts?.length ?? 0,
453
534
  territories: topTerritories,
535
+ forecastBreakdown,
536
+ partnerName,
537
+ partnerRole,
454
538
  };
455
539
  }
456
540
 
@@ -458,7 +542,10 @@ export function buildSalesforceHint(ctx: SalesforceContext | null): SalesforceHi
458
542
  // Markdown renderer
459
543
  // ---------------------------------------------------------------------------
460
544
 
461
- export function renderSalesforceContextMarkdown(ctx: SalesforceContext | null): string {
545
+ export function renderSalesforceContextMarkdown(
546
+ ctx: SalesforceContext | null,
547
+ profile?: { partner?: UserProfile["partner"]; territories?: string[]; role?: string },
548
+ ): string {
462
549
  if (!ctx) {
463
550
  return "No Salesforce context. Use `xcsh://salesforce?refresh=true` to discover.";
464
551
  }
@@ -584,6 +671,43 @@ export function renderSalesforceContextMarkdown(ctx: SalesforceContext | null):
584
671
  }
585
672
  }
586
673
 
674
+ // Action Needed: guide user to set identity facts in user-profile.json
675
+ const needsConfirmation: string[] = [];
676
+ const profileHasPartner = !!profile?.partner?.name;
677
+ const profileHasTerritories = !!profile?.territories?.length;
678
+ if (!profileHasPartner && ctx.discoveredPartner) {
679
+ needsConfirmation.push(
680
+ `- **Partner:** Discovered "${ctx.discoveredPartner.name}" (${ctx.discoveredPartner.role}) from opportunity co-membership.`,
681
+ );
682
+ needsConfirmation.push(
683
+ ` To confirm: add \`"partner": { "name": "${ctx.discoveredPartner.name}", "role": "${ctx.discoveredPartner.role}" }\` to \`~/.xcsh/user-profile.json\``,
684
+ );
685
+ }
686
+ if (!profileHasTerritories && ctx.territories?.length) {
687
+ const examples = ctx.territories
688
+ .slice(0, 2)
689
+ .map(t => `"${t}"`)
690
+ .join(", ");
691
+ needsConfirmation.push(
692
+ `- **Territories:** ${ctx.territories.length} discovered from pipeline. Primary ones are unknown.`,
693
+ );
694
+ needsConfirmation.push(` To confirm: add \`"territories": [${examples}]\` to \`~/.xcsh/user-profile.json\``);
695
+ }
696
+ if (!profile?.role) {
697
+ needsConfirmation.push(
698
+ `- **Role:** Not set. Add \`"role": "SE"\` (or AE/CSM/SA/etc.) to \`~/.xcsh/user-profile.json\``,
699
+ );
700
+ }
701
+ if (needsConfirmation.length > 0) {
702
+ sections.push("\n## Setup: Identity Facts");
703
+ sections.push(
704
+ "\nThe following are unknown. Set them in `~/.xcsh/user-profile.json` to get accurate partner-scoped pipeline reports.\n",
705
+ );
706
+ for (const line of needsConfirmation) {
707
+ sections.push(line);
708
+ }
709
+ }
710
+
587
711
  // Footer
588
712
  sections.push(`\n---\n*Collected: ${ctx.collectedAt}*`);
589
713
 
@@ -46,6 +46,22 @@ export interface UserProfile {
46
46
  image?: string;
47
47
  sameAs?: string[];
48
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. */
50
+ role?: string;
51
+ /**
52
+ * User-authored: confirmed partner (AE/SE counterpart, CSM, etc.).
53
+ * Set manually in user-profile.json. Survives Salesforce re-seeds.
54
+ */
55
+ partner?: {
56
+ /** Salesforce User Id — used to scope pipeline queries */
57
+ id?: string;
58
+ name: string;
59
+ title?: string;
60
+ /** Short role label, e.g. 'AE', 'SE', 'CSM' */
61
+ role?: string;
62
+ };
63
+ /** User-authored: primary territory names. Exact Salesforce field values. Scopes pipeline reports. */
64
+ territories?: string[];
49
65
  observations?: UserProfileObservation[];
50
66
  sources?: { salesforce?: string; github?: string; system?: string; conversation?: string };
51
67
  updatedAt?: string;
@@ -231,6 +231,8 @@ export async function generatePipelineReport(options: PipelineReportOptions): Pr
231
231
  const staleCutoff = options.staleCutoff;
232
232
 
233
233
  const userFilter = buildUserFilter(userIds);
234
+ const ownerFilter =
235
+ userIds.length === 1 ? `OwnerId = '${userIds[0]}'` : `OwnerId IN (${userIds.map(id => `'${id}'`).join(",")})`;
234
236
  const skuFilter = buildSkuFilter(skuPrefixes);
235
237
 
236
238
  const fields = [
@@ -245,7 +247,7 @@ export async function generatePipelineReport(options: PipelineReportOptions): Pr
245
247
  const quarterDateFilter = `Opportunity.CloseDate >= ${quarterStart} AND Opportunity.CloseDate <= ${quarterEnd}`;
246
248
  const inPlayDateFilter = staleCutoff ? `Opportunity.CloseDate >= ${staleCutoff}` : quarterDateFilter;
247
249
 
248
- const teamScope = `OpportunityId IN (SELECT OpportunityId FROM OpportunityTeamMember WHERE ${userFilter})`;
250
+ const teamScope = `(OpportunityId IN (SELECT OpportunityId FROM OpportunityTeamMember WHERE ${userFilter}) OR ${ownerFilter})`;
249
251
 
250
252
  // In-play: uses staleCutoff if set, otherwise quarter dates
251
253
  // Booked: always quarter dates
@@ -280,7 +282,7 @@ export async function generatePipelineReport(options: PipelineReportOptions): Pr
280
282
  const renewalDateFilter = staleCutoff
281
283
  ? `CloseDate >= ${staleCutoff}`
282
284
  : `CloseDate >= ${quarterStart} AND CloseDate <= ${quarterEnd}`;
283
- const renewalWhere = `Id IN (SELECT OpportunityId FROM OpportunityTeamMember WHERE ${userFilter}) AND IsClosed = false AND Renewal__c = true AND ${renewalDateFilter} AND ForecastCategoryName != 'Omitted' AND (True_ACV__c > 1 OR Upsell_ACV__c > 1 OR Amount > 1)`;
285
+ const renewalWhere = `(Id IN (SELECT OpportunityId FROM OpportunityTeamMember WHERE ${userFilter}) OR ${ownerFilter}) AND IsClosed = false AND Renewal__c = true AND ${renewalDateFilter} AND ForecastCategoryName != 'Omitted' AND (True_ACV__c > 1 OR Upsell_ACV__c > 1 OR Amount > 1)`;
284
286
 
285
287
  const renewalRecords = await runSfQuery(
286
288
  `SELECT ${renewalFields} FROM Opportunity WHERE ${renewalWhere} ORDER BY True_ACV__c DESC NULLS LAST`,
@@ -12,53 +12,6 @@ function fmtCompact(val: number): string {
12
12
  return `$${val.toFixed(0)}`;
13
13
  }
14
14
 
15
- interface ColumnDef {
16
- header: string;
17
- key: keyof AccountRow;
18
- }
19
-
20
- function renderSection(title: string, section: SectionData, columns: ColumnDef[]): string {
21
- if (section.accounts.length === 0) return "";
22
-
23
- const lines: string[] = [];
24
- lines.push(`## ${title}`);
25
- lines.push("");
26
-
27
- const hdrs = ["Account", ...columns.map(c => c.header)];
28
- lines.push(`| ${hdrs.join(" | ")} |`);
29
- lines.push(`|${[":---", ...columns.map(() => "---:")].join("|")}|`);
30
-
31
- // Group by territory
32
- const byTerritory = new Map<string, AccountRow[]>();
33
- for (const row of section.accounts) {
34
- const t = row.territory || "Unassigned";
35
- const arr = byTerritory.get(t) ?? [];
36
- arr.push(row);
37
- byTerritory.set(t, arr);
38
- }
39
-
40
- for (const [territory, rows] of byTerritory) {
41
- if (byTerritory.size > 1) {
42
- lines.push(`| **\u2014 ${territory} \u2014** | ${columns.map(() => "").join(" | ")} |`);
43
- }
44
- for (const row of rows) {
45
- const vals = columns.map(c => fmtCurrency(row[c.key] as number));
46
- lines.push(`| ${row.name} | ${vals.join(" | ")} |`);
47
- }
48
- }
49
-
50
- const totalVals = columns.map(c => {
51
- const v = (section.totals as unknown as Record<string, number>)[c.key] ?? 0;
52
- return `**${fmtCurrency(v)}**`;
53
- });
54
- lines.push(`| **Total** | ${totalVals.join(" | ")} |`);
55
- lines.push("");
56
- lines.push(`**Quota Total (Platform + Shape/DI):** ${fmtCompact(section.quotaTotal)}`);
57
- lines.push("");
58
-
59
- return lines.join("\n");
60
- }
61
-
62
15
  function renderAnomalies(anomalies: DataAnomaly[]): string {
63
16
  if (anomalies.length === 0) return "";
64
17
 
@@ -93,6 +46,26 @@ export function renderPipelineReport(data: PipelineReportData, _instanceUrl: str
93
46
  lines.push(`**Line items:** ${data.lineItemCount} | **SKUs:** ${data.skusFound.length}`);
94
47
  lines.push("");
95
48
 
49
+ // Executive summary
50
+ const summaryParts: string[] = [];
51
+ if (data.netNew.quotaTotal > 0) {
52
+ summaryParts.push(`Net New: ${fmtCompact(data.netNew.quotaTotal)} (${data.netNew.accounts.length} accounts)`);
53
+ }
54
+ if (data.renewals.quotaTotal > 0) {
55
+ summaryParts.push(
56
+ `Renewals: ${fmtCompact(data.renewals.quotaTotal)} (${data.renewals.accounts.length} accounts)`,
57
+ );
58
+ }
59
+ if (data.booked.quotaTotal > 0) {
60
+ summaryParts.push(`Booked: ${fmtCompact(data.booked.quotaTotal)}`);
61
+ } else {
62
+ summaryParts.push("Booked: $0");
63
+ }
64
+ if (summaryParts.length > 0) {
65
+ lines.push(`**Summary:** ${summaryParts.join(" | ")}`);
66
+ lines.push("");
67
+ }
68
+
96
69
  // Helper: render one product group (Platform or Point) as its own sub-table
97
70
  function renderProductGroup(
98
71
  sectionTitle: string,
@@ -146,7 +119,14 @@ export function renderPipelineReport(data: PipelineReportData, _instanceUrl: str
146
119
  }
147
120
 
148
121
  const booked = renderBiSection("Closed \u2014 Booked This Quarter", data.booked);
149
- if (booked) lines.push(booked);
122
+ if (booked) {
123
+ lines.push(booked);
124
+ } else {
125
+ lines.push("## Closed \u2014 Booked This Quarter");
126
+ lines.push("");
127
+ lines.push("No deals closed this quarter.");
128
+ lines.push("");
129
+ }
150
130
 
151
131
  const netNew = renderBiSection("Open Pipeline \u2014 Net New", data.netNew);
152
132
  if (netNew) lines.push(netNew);
@@ -152,16 +152,15 @@ Available F5 XC documentation topics: {{knowledgeTopics}}.
152
152
  {{#if userProfile}}
153
153
  ## Primary Human
154
154
 
155
- {{userProfile.name}}, {{userProfile.role}}, {{userProfile.org}}.
156
- `xcsh://user`. **MUST** read when: identity, communications, personal identifiers. **SHOULD NOT** for routine technical work.
155
+ {{userProfile.name}}, {{userProfile.role}}, {{userProfile.org}}. `xcsh://user` **MUST** read: identity, comms, PII. **SHOULD NOT** routine work.
157
156
  {{/if}}
158
157
 
159
158
  {{#if computerProfile}}
160
- `xcsh://computer`. {{computerProfile.ramGB}}GB RAM, {{computerProfile.cpu}}, {{computerProfile.os}}{{#if computerProfile.cores}} ({{computerProfile.cores}} cores){{/if}}{{#if computerProfile.shell}}, {{computerProfile.shell}}{{/if}}.{{#if computerProfile.managed}} Managed.{{/if}}
159
+ `xcsh://computer`. {{computerProfile.ramGB}}GB, {{computerProfile.cpu}}, {{computerProfile.os}}{{#if computerProfile.shell}}, {{computerProfile.shell}}{{/if}}.{{#if computerProfile.managed}} Managed{{#unless computerProfile.admin}} (not admin{{#if computerProfile.endpointAgentCount}}, {{computerProfile.endpointAgentCount}} agents{{/if}}){{/unless}}.{{/if}}
161
160
  {{/if}}
162
161
 
163
162
  {{#if salesforceHint}}
164
- `xcsh://salesforce`. {{salesforceHint.dealCount}} deals, {{salesforceHint.pipelineTotal}} pipeline, {{salesforceHint.accountCount}} accounts{{#if salesforceHint.territories}} ({{salesforceHint.territories}}){{/if}}.
163
+ `xcsh://salesforce`. {{salesforceHint.pipelineTotal}}{{#if salesforceHint.territories}} ({{salesforceHint.territories}}){{/if}}.{{#if salesforceHint.partnerName}} {{salesforceHint.partnerRole}}: {{salesforceHint.partnerName}}.{{/if}}{{#if salesforceHint.forecastBreakdown}} {{salesforceHint.forecastBreakdown}}.{{/if}}
165
164
  {{/if}}
166
165
 
167
166
  {{#if contextFiles.length}}
package/src/sdk.ts CHANGED
@@ -74,7 +74,7 @@ import {
74
74
  } from "./internal-urls";
75
75
  import { buildComputerHint, loadComputerProfile } from "./internal-urls/computer-profile";
76
76
  import { buildSalesforceHint, loadSalesforceContext } from "./internal-urls/salesforce-context";
77
- import { loadProfile } from "./internal-urls/user-profile";
77
+ import { loadProfile, type UserProfile } from "./internal-urls/user-profile";
78
78
  import { disposeAllKernelSessions, disposeKernelSessionsByOwner } from "./ipy/executor";
79
79
  import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "./lsp/startup-events";
80
80
  import { discoverAndLoadMCPTools, type MCPManager, type MCPToolsLoadResult } from "./mcp";
@@ -1453,25 +1453,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1453
1453
  }
1454
1454
  appendPrompt = parts.join("\n\n");
1455
1455
  }
1456
- // Load compact user profile for system prompt hint
1457
- let userProfile: { name: string; role: string; org: string } | undefined;
1456
+ // Load user profile — used for system prompt hint AND Salesforce context
1457
+ let _profile: UserProfile;
1458
1458
  try {
1459
- const _profile = await loadProfile();
1460
- if (_profile.givenName || _profile.familyName) {
1461
- const _name = [_profile.givenName, _profile.familyName].filter(Boolean).join(" ");
1462
- if (_name) {
1463
- userProfile = {
1464
- name: _name,
1465
- role: _profile.jobTitle ?? "",
1466
- org:
1467
- typeof _profile.worksFor === "string"
1468
- ? _profile.worksFor
1469
- : ((_profile.worksFor as { name?: string } | undefined)?.name ?? ""),
1470
- };
1471
- }
1472
- }
1459
+ _profile = await loadProfile();
1473
1460
  } catch {
1474
- // No profile — hint block omitted
1461
+ _profile = {};
1462
+ }
1463
+ let userProfile: { name: string; role: string; org: string } | undefined;
1464
+ if (_profile.givenName || _profile.familyName) {
1465
+ const _name = [_profile.givenName, _profile.familyName].filter(Boolean).join(" ");
1466
+ if (_name) {
1467
+ userProfile = {
1468
+ name: _name,
1469
+ role: _profile.role ?? _profile.jobTitle ?? "",
1470
+ org:
1471
+ typeof _profile.worksFor === "string"
1472
+ ? _profile.worksFor
1473
+ : ((_profile.worksFor as { name?: string } | undefined)?.name ?? ""),
1474
+ };
1475
+ }
1475
1476
  }
1476
1477
 
1477
1478
  // Load compact computer profile hint for system prompt
@@ -1493,13 +1494,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1493
1494
  // No computer profile — hint block omitted
1494
1495
  }
1495
1496
 
1496
- // Load compact Salesforce pipeline hint for system prompt
1497
+ // Load compact Salesforce pipeline hint profile provides partner/territory context
1497
1498
  let salesforceHint:
1498
1499
  | { pipelineTotal: string; dealCount: number; accountCount: number; territories?: string }
1499
1500
  | undefined;
1500
1501
  try {
1501
1502
  const _sfContext = await loadSalesforceContext();
1502
- salesforceHint = buildSalesforceHint(_sfContext) ?? undefined;
1503
+ salesforceHint = buildSalesforceHint(_sfContext, _profile) ?? undefined;
1503
1504
  } catch {
1504
1505
  // No Salesforce context — hint block omitted
1505
1506
  }
@@ -474,6 +474,9 @@ export interface BuildSystemPromptOptions {
474
474
  shell?: string;
475
475
  diskFree?: string;
476
476
  model?: string;
477
+ managed?: boolean;
478
+ admin?: boolean;
479
+ endpointAgentCount?: number;
477
480
  };
478
481
  /** Compact Salesforce pipeline hint. Omit when no Salesforce context cached. */
479
482
  salesforceHint?: {
@@ -481,6 +484,12 @@ export interface BuildSystemPromptOptions {
481
484
  dealCount: number;
482
485
  accountCount: number;
483
486
  territories?: string;
487
+ /** Compact forecast breakdown, e.g. 'Commit $500K + Best $472K + Pipe $1.9M' */
488
+ forecastBreakdown?: string;
489
+ /** Confirmed AE partner name */
490
+ partnerName?: string;
491
+ /** Partner role abbreviation: 'AE', 'SE', 'other' */
492
+ partnerRole?: string;
484
493
  };
485
494
  knowledgeTopics?: string;
486
495
  contextSkillDirs?: string[];