@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 +7 -7
- package/src/internal-urls/build-info-runtime.ts +1 -1
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/computer-profile.ts +6 -1
- package/src/internal-urls/salesforce-context.ts +142 -18
- package/src/internal-urls/user-profile.ts +16 -0
- package/src/pipeline-report/generator.ts +4 -2
- package/src/pipeline-report/renderer.ts +28 -48
- package/src/prompts/system/system-prompt.md +3 -4
- package/src/sdk.ts +21 -20
- package/src/system-prompt.ts +9 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "18.49.
|
|
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.
|
|
52
|
-
"@f5xc-salesdemos/pi-agent-core": "18.49.
|
|
53
|
-
"@f5xc-salesdemos/pi-ai": "18.49.
|
|
54
|
-
"@f5xc-salesdemos/pi-natives": "18.49.
|
|
55
|
-
"@f5xc-salesdemos/pi-tui": "18.49.
|
|
56
|
-
"@f5xc-salesdemos/pi-utils": "18.49.
|
|
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
|
-
"
|
|
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.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "18.49.1",
|
|
21
|
+
"commit": "e8fa2d917ec4476ac2e1bb9f4141992961084755",
|
|
22
|
+
"shortCommit": "e8fa2d9",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v18.49.
|
|
25
|
-
"commitDate": "2026-05-
|
|
26
|
-
"buildDate": "2026-05-
|
|
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/
|
|
32
|
-
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.49.
|
|
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]
|
|
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
|
-
/**
|
|
16
|
-
role:
|
|
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
|
-
*
|
|
56
|
-
*
|
|
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(
|
|
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
|
|
443
|
-
|
|
444
|
-
? `$${(
|
|
445
|
-
:
|
|
446
|
-
? `$${(
|
|
447
|
-
: `$${
|
|
448
|
-
const
|
|
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(
|
|
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)
|
|
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
|
|
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.
|
|
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
|
|
1457
|
-
let
|
|
1456
|
+
// Load user profile — used for system prompt hint AND Salesforce context
|
|
1457
|
+
let _profile: UserProfile;
|
|
1458
1458
|
try {
|
|
1459
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/src/system-prompt.ts
CHANGED
|
@@ -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[];
|