@f5xc-salesdemos/xcsh 18.41.1 → 18.43.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.
- package/package.json +7 -7
- package/src/internal-urls/api-spec-resolve.ts +2 -0
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/index.ts +2 -0
- package/src/internal-urls/profile-collectors.ts +228 -0
- package/src/internal-urls/user-profile.ts +287 -0
- package/src/internal-urls/xcsh-protocol.ts +27 -1
- package/src/prompts/tools/bash.md +4 -4
- package/src/prompts/tools/sf-query.md +1 -1
- package/src/prompts/tools/sf-setup.md +4 -4
- package/src/tools/sf/formatters.ts +1 -49
- package/src/tools/sf/types.ts +0 -27
- package/src/tools/sf.ts +9 -76
- package/src/tools/sf/config.ts +0 -44
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "18.
|
|
4
|
+
"version": "18.43.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",
|
|
@@ -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.
|
|
52
|
-
"@f5xc-salesdemos/pi-agent-core": "18.
|
|
53
|
-
"@f5xc-salesdemos/pi-ai": "18.
|
|
54
|
-
"@f5xc-salesdemos/pi-natives": "18.
|
|
55
|
-
"@f5xc-salesdemos/pi-tui": "18.
|
|
56
|
-
"@f5xc-salesdemos/pi-utils": "18.
|
|
51
|
+
"@f5xc-salesdemos/xcsh-stats": "18.43.0",
|
|
52
|
+
"@f5xc-salesdemos/pi-agent-core": "18.43.0",
|
|
53
|
+
"@f5xc-salesdemos/pi-ai": "18.43.0",
|
|
54
|
+
"@f5xc-salesdemos/pi-natives": "18.43.0",
|
|
55
|
+
"@f5xc-salesdemos/pi-tui": "18.43.0",
|
|
56
|
+
"@f5xc-salesdemos/pi-utils": "18.43.0",
|
|
57
57
|
"@sinclair/typebox": "^0.34",
|
|
58
58
|
"@xterm/headless": "^6.0",
|
|
59
59
|
"ajv": "^8.18",
|
|
@@ -412,6 +412,8 @@ function formatFieldConstraints(prop: Record<string, unknown>): string {
|
|
|
412
412
|
if (c.maxItems != null) parts.push(`maxItems: ${c.maxItems}`);
|
|
413
413
|
if (c.format) parts.push(`format: ${c.format}`);
|
|
414
414
|
if (Array.isArray(c.enum)) parts.push(`enum: ${(c.enum as string[]).join(", ")}`);
|
|
415
|
+
const meta = c.metadata as Record<string, unknown> | undefined;
|
|
416
|
+
if (meta?.note) parts.push(`note: ${String(meta.note)}`);
|
|
415
417
|
return parts.join(", ");
|
|
416
418
|
}
|
|
417
419
|
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "18.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "18.43.0",
|
|
21
|
+
"commit": "3b728b77f3756587bfb90eb1e264dc60890ef4bd",
|
|
22
|
+
"shortCommit": "3b728b7",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v18.
|
|
25
|
-
"commitDate": "2026-05-
|
|
26
|
-
"buildDate": "2026-05-
|
|
24
|
+
"tag": "v18.43.0",
|
|
25
|
+
"commitDate": "2026-05-05T18:56:20Z",
|
|
26
|
+
"buildDate": "2026-05-05T19:16:23.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.
|
|
31
|
+
"commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/3b728b77f3756587bfb90eb1e264dc60890ef4bd",
|
|
32
|
+
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.43.0"
|
|
33
33
|
};
|
|
@@ -32,8 +32,10 @@ export * from "./local-protocol";
|
|
|
32
32
|
export * from "./mcp-protocol";
|
|
33
33
|
export * from "./memory-protocol";
|
|
34
34
|
export * from "./parse";
|
|
35
|
+
export * from "./profile-collectors";
|
|
35
36
|
export * from "./router";
|
|
36
37
|
export * from "./rule-protocol";
|
|
37
38
|
export * from "./skill-protocol";
|
|
38
39
|
export type * from "./types";
|
|
40
|
+
export * from "./user-profile";
|
|
39
41
|
export * from "./xcsh-protocol";
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { $which, logger } from "@f5xc-salesdemos/pi-utils";
|
|
2
|
+
import { $ } from "bun";
|
|
3
|
+
|
|
4
|
+
import type { UserProfile } from "./user-profile";
|
|
5
|
+
|
|
6
|
+
export interface ProfileCollector {
|
|
7
|
+
/** Unique identifier for this collector */
|
|
8
|
+
readonly id: string;
|
|
9
|
+
/** Human-readable name */
|
|
10
|
+
readonly name: string;
|
|
11
|
+
/** Check if this collector can run (binary exists, platform ok, etc.) */
|
|
12
|
+
available(): Promise<boolean>;
|
|
13
|
+
/** Run the collector and return partial profile fields to merge */
|
|
14
|
+
collect(): Promise<Partial<UserProfile>>;
|
|
15
|
+
}
|
|
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
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// GitHub
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
const githubCollector: ProfileCollector = {
|
|
125
|
+
id: "github",
|
|
126
|
+
name: "GitHub",
|
|
127
|
+
|
|
128
|
+
async available(): Promise<boolean> {
|
|
129
|
+
if (!$which("gh")) return false;
|
|
130
|
+
try {
|
|
131
|
+
const proc = await $`gh auth status`.quiet().nothrow();
|
|
132
|
+
return proc.exitCode === 0;
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
async collect(): Promise<Partial<UserProfile>> {
|
|
139
|
+
try {
|
|
140
|
+
const proc = await $`gh api user`.quiet().nothrow();
|
|
141
|
+
if (proc.exitCode !== 0) return {};
|
|
142
|
+
|
|
143
|
+
const data = JSON.parse(proc.stdout.toString()) as Record<string, unknown>;
|
|
144
|
+
const profile: Partial<UserProfile> = {};
|
|
145
|
+
const sameAs: string[] = [];
|
|
146
|
+
|
|
147
|
+
const login = data.login as string | undefined;
|
|
148
|
+
if (login) {
|
|
149
|
+
profile.identifiers = { ...profile.identifiers, github: login };
|
|
150
|
+
sameAs.push(`https://github.com/${login}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const name = data.name as string | undefined;
|
|
154
|
+
if (name) {
|
|
155
|
+
const parts = name.split(" ");
|
|
156
|
+
if (parts.length >= 2) {
|
|
157
|
+
profile.givenName = parts[0];
|
|
158
|
+
profile.familyName = parts.slice(1).join(" ");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const email = data.email as string | undefined;
|
|
163
|
+
if (email) profile.email = email;
|
|
164
|
+
|
|
165
|
+
const bio = data.bio as string | undefined;
|
|
166
|
+
if (bio) profile.description = bio;
|
|
167
|
+
|
|
168
|
+
const blog = data.blog as string | undefined;
|
|
169
|
+
if (blog) {
|
|
170
|
+
profile.url = blog;
|
|
171
|
+
sameAs.push(blog);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const twitterUsername = data.twitter_username as string | undefined;
|
|
175
|
+
if (twitterUsername) {
|
|
176
|
+
profile.identifiers = { ...profile.identifiers, twitter: twitterUsername };
|
|
177
|
+
sameAs.push(`https://x.com/${twitterUsername}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (sameAs.length > 0) profile.sameAs = sameAs;
|
|
181
|
+
|
|
182
|
+
return profile;
|
|
183
|
+
} catch (err: unknown) {
|
|
184
|
+
logger.debug("github collector failed", { error: err });
|
|
185
|
+
return {};
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// System (macOS)
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
const systemCollector: ProfileCollector = {
|
|
195
|
+
id: "system",
|
|
196
|
+
name: "System",
|
|
197
|
+
|
|
198
|
+
async available(): Promise<boolean> {
|
|
199
|
+
return process.platform === "darwin";
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
async collect(): Promise<Partial<UserProfile>> {
|
|
203
|
+
try {
|
|
204
|
+
const proc = await $`defaults read NSGlobalDomain AppleLanguages`.quiet().nothrow();
|
|
205
|
+
if (proc.exitCode !== 0) return {};
|
|
206
|
+
|
|
207
|
+
const raw = proc.stdout.toString().trim();
|
|
208
|
+
// Plist array format: (\n "en-US",\n "fr-FR"\n)
|
|
209
|
+
const inner = raw.replace(/^\(\s*/, "").replace(/\s*\)$/, "");
|
|
210
|
+
const languages = inner
|
|
211
|
+
.split(",")
|
|
212
|
+
.map(s => s.trim().replace(/^"/, "").replace(/"$/, ""))
|
|
213
|
+
.filter(s => s.length > 0);
|
|
214
|
+
|
|
215
|
+
if (languages.length === 0) return {};
|
|
216
|
+
return { knowsLanguage: languages };
|
|
217
|
+
} catch (err: unknown) {
|
|
218
|
+
logger.debug("system collector failed", { error: err });
|
|
219
|
+
return {};
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Registry
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
export const PROFILE_COLLECTORS: readonly ProfileCollector[] = [salesforceCollector, githubCollector, systemCollector];
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { isEnoent, logger } from "@f5xc-salesdemos/pi-utils";
|
|
4
|
+
import { PROFILE_COLLECTORS } from "./profile-collectors";
|
|
5
|
+
|
|
6
|
+
export interface UserProfileObservation {
|
|
7
|
+
key: string;
|
|
8
|
+
value: string;
|
|
9
|
+
source?: string;
|
|
10
|
+
observedAt?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UserProfile {
|
|
14
|
+
givenName?: string;
|
|
15
|
+
familyName?: string;
|
|
16
|
+
additionalName?: string;
|
|
17
|
+
email?: string;
|
|
18
|
+
telephone?: string;
|
|
19
|
+
jobTitle?: string;
|
|
20
|
+
department?: string;
|
|
21
|
+
division?: string;
|
|
22
|
+
worksFor?: { name?: string; url?: string };
|
|
23
|
+
manager?: { givenName?: string; familyName?: string; email?: string };
|
|
24
|
+
address?: {
|
|
25
|
+
streetAddress?: string;
|
|
26
|
+
addressLocality?: string;
|
|
27
|
+
addressRegion?: string;
|
|
28
|
+
postalCode?: string;
|
|
29
|
+
addressCountry?: string;
|
|
30
|
+
};
|
|
31
|
+
birthDate?: string;
|
|
32
|
+
birthPlace?: {
|
|
33
|
+
addressLocality?: string;
|
|
34
|
+
addressRegion?: string;
|
|
35
|
+
addressCountry?: string;
|
|
36
|
+
};
|
|
37
|
+
nationality?: string;
|
|
38
|
+
gender?: string;
|
|
39
|
+
knowsLanguage?: string[];
|
|
40
|
+
spouse?: { givenName?: string; familyName?: string };
|
|
41
|
+
children?: Array<{ givenName?: string; familyName?: string; birthDate?: string }>;
|
|
42
|
+
parent?: Array<{ givenName?: string; familyName?: string }>;
|
|
43
|
+
sibling?: Array<{ givenName?: string; familyName?: string }>;
|
|
44
|
+
url?: string;
|
|
45
|
+
description?: string;
|
|
46
|
+
image?: string;
|
|
47
|
+
sameAs?: string[];
|
|
48
|
+
identifiers?: { github?: string; twitter?: string; salesforceId?: string };
|
|
49
|
+
observations?: UserProfileObservation[];
|
|
50
|
+
sources?: { salesforce?: string; github?: string; system?: string; conversation?: string };
|
|
51
|
+
updatedAt?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const PROFILE_PATH = path.join(os.homedir(), ".xcsh", "user-profile.json");
|
|
55
|
+
|
|
56
|
+
export async function loadProfile(): Promise<UserProfile> {
|
|
57
|
+
try {
|
|
58
|
+
return (await Bun.file(PROFILE_PATH).json()) as UserProfile;
|
|
59
|
+
} catch (err: unknown) {
|
|
60
|
+
if (isEnoent(err)) return {};
|
|
61
|
+
logger.warn("Failed to load user profile", { error: err });
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function saveProfile(profile: UserProfile): Promise<void> {
|
|
67
|
+
profile.updatedAt = new Date().toISOString();
|
|
68
|
+
await Bun.write(PROFILE_PATH, JSON.stringify(profile, null, 2));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function mergeProfile(target: UserProfile, source: Partial<UserProfile>): void {
|
|
72
|
+
for (const [key, value] of Object.entries(source)) {
|
|
73
|
+
if (value === undefined || value === null) continue;
|
|
74
|
+
const k = key as keyof UserProfile;
|
|
75
|
+
if (k === "sources" || k === "observations" || k === "updatedAt") continue;
|
|
76
|
+
if (k === "sameAs") {
|
|
77
|
+
// Merge arrays without duplicates
|
|
78
|
+
if (!target.sameAs) target.sameAs = [];
|
|
79
|
+
for (const url of value as string[]) {
|
|
80
|
+
if (!target.sameAs.includes(url)) target.sameAs.push(url);
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (k === "knowsLanguage") {
|
|
85
|
+
if (!target.knowsLanguage) target.knowsLanguage = value as string[];
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// For objects (worksFor, manager, address, etc.), merge sub-fields
|
|
89
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
90
|
+
const existing = target[k] as Record<string, unknown> | undefined;
|
|
91
|
+
if (!existing) {
|
|
92
|
+
(target as Record<string, unknown>)[k] = value;
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// For simple values, don't overwrite
|
|
97
|
+
if (target[k] === undefined || target[k] === null) {
|
|
98
|
+
(target as Record<string, unknown>)[k] = value;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function seedProfile(): Promise<UserProfile> {
|
|
104
|
+
const profile = await loadProfile();
|
|
105
|
+
if (!profile.sources) profile.sources = {};
|
|
106
|
+
|
|
107
|
+
for (const collector of PROFILE_COLLECTORS) {
|
|
108
|
+
try {
|
|
109
|
+
const isAvailable = await collector.available();
|
|
110
|
+
if (!isAvailable) {
|
|
111
|
+
logger.debug(`Profile collector '${collector.id}' not available, skipping`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const partial = await collector.collect();
|
|
115
|
+
mergeProfile(profile, partial);
|
|
116
|
+
(profile.sources as Record<string, string>)[collector.id] = new Date().toISOString();
|
|
117
|
+
logger.debug(`Profile collector '${collector.id}' completed`);
|
|
118
|
+
} catch (err: unknown) {
|
|
119
|
+
logger.debug(`Profile collector '${collector.id}' failed`, { error: err });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await saveProfile(profile);
|
|
124
|
+
return profile;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function formatAddress(addr: NonNullable<UserProfile["address"]>): string {
|
|
128
|
+
const parts: string[] = [];
|
|
129
|
+
if (addr.streetAddress) parts.push(addr.streetAddress);
|
|
130
|
+
const cityState = [addr.addressLocality, addr.addressRegion].filter(Boolean).join(", ");
|
|
131
|
+
if (cityState) parts.push(cityState);
|
|
132
|
+
if (addr.postalCode) parts.push(addr.postalCode);
|
|
133
|
+
if (addr.addressCountry) parts.push(addr.addressCountry);
|
|
134
|
+
return parts.join(", ");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function hasValues(obj: Record<string, unknown> | undefined): boolean {
|
|
138
|
+
if (!obj) return false;
|
|
139
|
+
return Object.values(obj).some(v => v !== undefined && v !== null && v !== "");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function renderProfileMarkdown(profile: UserProfile): string {
|
|
143
|
+
const sections: string[] = [];
|
|
144
|
+
|
|
145
|
+
sections.push("# User Profile\n");
|
|
146
|
+
|
|
147
|
+
const isEmpty = !profile.givenName && !profile.familyName && !profile.email && !profile.jobTitle;
|
|
148
|
+
if (isEmpty) {
|
|
149
|
+
sections.push(
|
|
150
|
+
"No profile data yet. Use `xcsh://user?seed=true` to populate from Salesforce, GitHub, and system sources.\n",
|
|
151
|
+
);
|
|
152
|
+
sections.push("Profile facts can also be added progressively during conversation.\n");
|
|
153
|
+
return sections.join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Identity
|
|
157
|
+
const identityLines: string[] = [];
|
|
158
|
+
const fullName = [profile.givenName, profile.additionalName, profile.familyName].filter(Boolean).join(" ");
|
|
159
|
+
if (fullName) identityLines.push(`- **Name:** ${fullName}`);
|
|
160
|
+
if (profile.email) identityLines.push(`- **Email:** ${profile.email}`);
|
|
161
|
+
if (profile.telephone) identityLines.push(`- **Phone:** ${profile.telephone}`);
|
|
162
|
+
if (profile.image) identityLines.push(`- **Avatar:** ${profile.image}`);
|
|
163
|
+
if (identityLines.length > 0) {
|
|
164
|
+
sections.push("## Identity\n");
|
|
165
|
+
sections.push(identityLines.join("\n"));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Employment
|
|
169
|
+
const employmentLines: string[] = [];
|
|
170
|
+
if (profile.jobTitle) employmentLines.push(`- **Title:** ${profile.jobTitle}`);
|
|
171
|
+
if (profile.department) employmentLines.push(`- **Department:** ${profile.department}`);
|
|
172
|
+
if (profile.division) employmentLines.push(`- **Division:** ${profile.division}`);
|
|
173
|
+
if (profile.worksFor?.name) {
|
|
174
|
+
const org = profile.worksFor.url ? `[${profile.worksFor.name}](${profile.worksFor.url})` : profile.worksFor.name;
|
|
175
|
+
employmentLines.push(`- **Organization:** ${org}`);
|
|
176
|
+
}
|
|
177
|
+
if (profile.manager) {
|
|
178
|
+
const mgrName = [profile.manager.givenName, profile.manager.familyName].filter(Boolean).join(" ");
|
|
179
|
+
const mgrLine = profile.manager.email ? `${mgrName} (${profile.manager.email})` : mgrName;
|
|
180
|
+
if (mgrLine) employmentLines.push(`- **Manager:** ${mgrLine}`);
|
|
181
|
+
}
|
|
182
|
+
if (employmentLines.length > 0) {
|
|
183
|
+
sections.push("\n## Employment\n");
|
|
184
|
+
sections.push(employmentLines.join("\n"));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Address
|
|
188
|
+
if (hasValues(profile.address)) {
|
|
189
|
+
sections.push("\n## Address\n");
|
|
190
|
+
sections.push(`- ${formatAddress(profile.address!)}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Demographics
|
|
194
|
+
const demoLines: string[] = [];
|
|
195
|
+
if (profile.birthDate) demoLines.push(`- **Birth Date:** ${profile.birthDate}`);
|
|
196
|
+
if (profile.birthPlace && hasValues(profile.birthPlace)) {
|
|
197
|
+
const bp = [
|
|
198
|
+
profile.birthPlace.addressLocality,
|
|
199
|
+
profile.birthPlace.addressRegion,
|
|
200
|
+
profile.birthPlace.addressCountry,
|
|
201
|
+
]
|
|
202
|
+
.filter(Boolean)
|
|
203
|
+
.join(", ");
|
|
204
|
+
if (bp) demoLines.push(`- **Birth Place:** ${bp}`);
|
|
205
|
+
}
|
|
206
|
+
if (profile.nationality) demoLines.push(`- **Nationality:** ${profile.nationality}`);
|
|
207
|
+
if (profile.gender) demoLines.push(`- **Gender:** ${profile.gender}`);
|
|
208
|
+
if (profile.knowsLanguage && profile.knowsLanguage.length > 0) {
|
|
209
|
+
demoLines.push(`- **Languages:** ${profile.knowsLanguage.join(", ")}`);
|
|
210
|
+
}
|
|
211
|
+
if (demoLines.length > 0) {
|
|
212
|
+
sections.push("\n## Demographics\n");
|
|
213
|
+
sections.push(demoLines.join("\n"));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Family
|
|
217
|
+
const familyLines: string[] = [];
|
|
218
|
+
if (profile.spouse) {
|
|
219
|
+
const spouseName = [profile.spouse.givenName, profile.spouse.familyName].filter(Boolean).join(" ");
|
|
220
|
+
if (spouseName) familyLines.push(`- **Spouse:** ${spouseName}`);
|
|
221
|
+
}
|
|
222
|
+
if (profile.children && profile.children.length > 0) {
|
|
223
|
+
for (const child of profile.children) {
|
|
224
|
+
const childName = [child.givenName, child.familyName].filter(Boolean).join(" ");
|
|
225
|
+
const childLine = child.birthDate ? `${childName} (b. ${child.birthDate})` : childName;
|
|
226
|
+
if (childLine) familyLines.push(`- **Child:** ${childLine}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (profile.parent && profile.parent.length > 0) {
|
|
230
|
+
for (const p of profile.parent) {
|
|
231
|
+
const pName = [p.givenName, p.familyName].filter(Boolean).join(" ");
|
|
232
|
+
if (pName) familyLines.push(`- **Parent:** ${pName}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (profile.sibling && profile.sibling.length > 0) {
|
|
236
|
+
for (const s of profile.sibling) {
|
|
237
|
+
const sName = [s.givenName, s.familyName].filter(Boolean).join(" ");
|
|
238
|
+
if (sName) familyLines.push(`- **Sibling:** ${sName}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (familyLines.length > 0) {
|
|
242
|
+
sections.push("\n## Family\n");
|
|
243
|
+
sections.push(familyLines.join("\n"));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Online Presence
|
|
247
|
+
const onlineLines: string[] = [];
|
|
248
|
+
if (profile.url) onlineLines.push(`- **Website:** ${profile.url}`);
|
|
249
|
+
if (profile.description) onlineLines.push(`- **Bio:** ${profile.description}`);
|
|
250
|
+
if (profile.identifiers?.github) onlineLines.push(`- **GitHub:** ${profile.identifiers.github}`);
|
|
251
|
+
if (profile.identifiers?.twitter) onlineLines.push(`- **Twitter/X:** ${profile.identifiers.twitter}`);
|
|
252
|
+
if (profile.identifiers?.salesforceId) onlineLines.push(`- **Salesforce ID:** ${profile.identifiers.salesforceId}`);
|
|
253
|
+
if (profile.sameAs && profile.sameAs.length > 0) {
|
|
254
|
+
for (const link of profile.sameAs) {
|
|
255
|
+
onlineLines.push(`- **Profile:** ${link}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (onlineLines.length > 0) {
|
|
259
|
+
sections.push("\n## Online Presence\n");
|
|
260
|
+
sections.push(onlineLines.join("\n"));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Observations
|
|
264
|
+
if (profile.observations && profile.observations.length > 0) {
|
|
265
|
+
sections.push("\n## Observations\n");
|
|
266
|
+
sections.push("| Key | Value | Source | Observed |");
|
|
267
|
+
sections.push("|-----|-------|--------|----------|");
|
|
268
|
+
for (const obs of profile.observations) {
|
|
269
|
+
sections.push(`| ${obs.key} | ${obs.value} | ${obs.source ?? ""} | ${obs.observedAt ?? ""} |`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Sources
|
|
274
|
+
if (profile.sources && hasValues(profile.sources as unknown as Record<string, unknown>)) {
|
|
275
|
+
sections.push("\n---\n");
|
|
276
|
+
sections.push("**Sources:**");
|
|
277
|
+
const srcLines: string[] = [];
|
|
278
|
+
if (profile.sources.salesforce) srcLines.push(`Salesforce: ${profile.sources.salesforce}`);
|
|
279
|
+
if (profile.sources.github) srcLines.push(`GitHub: ${profile.sources.github}`);
|
|
280
|
+
if (profile.sources.system) srcLines.push(`System: ${profile.sources.system}`);
|
|
281
|
+
if (profile.sources.conversation) srcLines.push(`Conversation: ${profile.sources.conversation}`);
|
|
282
|
+
sections.push(srcLines.join(" | "));
|
|
283
|
+
if (profile.updatedAt) sections.push(`\n*Last updated: ${profile.updatedAt}*`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return sections.join("\n");
|
|
287
|
+
}
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* - xcsh://api-spec/glossary/ - Acronym glossary
|
|
17
17
|
* - xcsh://api-catalog/ - API operation catalog
|
|
18
18
|
* - xcsh://api-catalog/{category} - Category operations with curl templates
|
|
19
|
+
* - xcsh://user - Human user profile
|
|
20
|
+
* - xcsh://user?seed=true - Seed profile from sources and render
|
|
19
21
|
*/
|
|
20
22
|
import * as path from "node:path";
|
|
21
23
|
import { logger } from "@f5xc-salesdemos/pi-utils";
|
|
@@ -27,11 +29,13 @@ import type { ApiSpecIndex, OpenAPISpec } from "./api-spec-types";
|
|
|
27
29
|
import { getRuntimeBuildInfo, type RuntimeBuildInfo, renderAboutDoc } from "./build-info-runtime";
|
|
28
30
|
import { EMBEDDED_DOC_FILENAMES, EMBEDDED_DOCS } from "./docs-index.generated";
|
|
29
31
|
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
32
|
+
import { loadProfile, renderProfileMarkdown, seedProfile } from "./user-profile";
|
|
30
33
|
|
|
31
34
|
const SCHEME_PREFIX = "xcsh://";
|
|
32
35
|
const ABOUT_ROUTE = "about";
|
|
33
36
|
const API_SPEC_HOST = "api-spec";
|
|
34
37
|
const API_CATALOG_HOST = "api-catalog";
|
|
38
|
+
const USER_ROUTE = "user";
|
|
35
39
|
|
|
36
40
|
const EMPTY_INDEX: ApiSpecIndex = { version: "unavailable", timestamp: "", domains: [] };
|
|
37
41
|
const EMPTY_CATALOG_INDEX: ApiCatalogIndex = {
|
|
@@ -163,6 +167,10 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
|
|
|
163
167
|
return this.#getApiCatalogResolver().resolve(url);
|
|
164
168
|
}
|
|
165
169
|
|
|
170
|
+
if (host === USER_ROUTE) {
|
|
171
|
+
return this.#resolveUserProfile(url);
|
|
172
|
+
}
|
|
173
|
+
|
|
166
174
|
const pathname = url.rawPathname ?? url.pathname;
|
|
167
175
|
const filename = host ? (pathname && pathname !== "/" ? host + pathname : host) : "";
|
|
168
176
|
|
|
@@ -173,6 +181,22 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
|
|
|
173
181
|
return this.#readDoc(filename, url);
|
|
174
182
|
}
|
|
175
183
|
|
|
184
|
+
async #resolveUserProfile(url: InternalUrl): Promise<InternalResource> {
|
|
185
|
+
const params = new URLSearchParams(url.search);
|
|
186
|
+
const shouldSeed = params.get("seed") === "true";
|
|
187
|
+
|
|
188
|
+
const profile = shouldSeed ? await seedProfile() : await loadProfile();
|
|
189
|
+
const content = renderProfileMarkdown(profile);
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
url: url.href,
|
|
193
|
+
content,
|
|
194
|
+
contentType: "text/markdown",
|
|
195
|
+
size: Buffer.byteLength(content, "utf-8"),
|
|
196
|
+
sourcePath: `xcsh://${USER_ROUTE}`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
176
200
|
async #listDocs(url: InternalUrl): Promise<InternalResource> {
|
|
177
201
|
if (EMBEDDED_DOC_FILENAMES.length === 0) {
|
|
178
202
|
throw new Error("No documentation files found");
|
|
@@ -183,13 +207,15 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
|
|
|
183
207
|
const syntheticEntry = `- [${ABOUT_ROUTE}](${SCHEME_PREFIX}${ABOUT_ROUTE}) — identity and build fingerprint`;
|
|
184
208
|
const apiSpecEntry = `- [${API_SPEC_HOST}/](${SCHEME_PREFIX}${API_SPEC_HOST}/) — F5 XC API specifications (${specs.index.domains.length} domains, v${specs.version})`;
|
|
185
209
|
const apiCatalogEntry = `- [${API_CATALOG_HOST}/](${SCHEME_PREFIX}${API_CATALOG_HOST}/) — F5 XC API operation catalog (${catalog.summaries.length} categories, v${catalog.index.version})`;
|
|
210
|
+
const userEntry = `- [${USER_ROUTE}](${SCHEME_PREFIX}${USER_ROUTE}) — human user profile`;
|
|
186
211
|
const listing = [
|
|
187
212
|
syntheticEntry,
|
|
188
213
|
apiSpecEntry,
|
|
189
214
|
apiCatalogEntry,
|
|
215
|
+
userEntry,
|
|
190
216
|
...EMBEDDED_DOC_FILENAMES.map(f => `- [${f}](${SCHEME_PREFIX}${f})`),
|
|
191
217
|
].join("\n");
|
|
192
|
-
const totalCount = EMBEDDED_DOC_FILENAMES.length +
|
|
218
|
+
const totalCount = EMBEDDED_DOC_FILENAMES.length + 4;
|
|
193
219
|
const content = `# Documentation\n\n${totalCount} files available:\n\n${listing}\n`;
|
|
194
220
|
|
|
195
221
|
return {
|
|
@@ -46,19 +46,19 @@ You **MUST** use specialized tools instead of bash for ALL file operations:
|
|
|
46
46
|
|---|---|
|
|
47
47
|
|`cat file`, `head -n N file`|`read(path="file", limit=N)`|
|
|
48
48
|
|`cat -n file \|sed -n '50,150p'`|`read(path="file", offset=50, limit=100)`|
|
|
49
|
-
<!-- markdownlint-disable MD055 MD056
|
|
49
|
+
<!-- markdownlint-disable MD055 MD056 -→
|
|
50
50
|
{{#if hasGrep}}|`grep -A 20 'pat' file`|`grep(pattern="pat", path="file", post=20)`|
|
|
51
51
|
|`grep -rn 'pat' dir/`|`grep(pattern="pat", path="dir/")`|
|
|
52
52
|
|`rg 'pattern' dir/`|`grep(pattern="pattern", path="dir/")`|{{/if}}
|
|
53
53
|
{{#if hasFind}}|`find dir -name '*.ts'`|`find(pattern="dir/**/*.ts")`|{{/if}}
|
|
54
|
-
<!-- markdownlint-enable MD055 MD056
|
|
54
|
+
<!-- markdownlint-enable MD055 MD056 -→
|
|
55
55
|
|`ls dir/`|`read(path="dir/")`|
|
|
56
56
|
|`cat <<'EOF' > file`|`write(path="file", content="…")`|
|
|
57
57
|
|`sed -i 's/old/new/' file`|`edit(path="file", edits=[…])`|
|
|
58
58
|
|
|
59
|
-
<!-- markdownlint-disable MD055 MD056
|
|
59
|
+
<!-- markdownlint-disable MD055 MD056 -→
|
|
60
60
|
{{#if hasAstEdit}}|`sed -i 's/oldFn(/newFn(/' src/*.ts`|`ast_edit({ops:[{pat:"oldFn($$$A)", out:"newFn($$$A)"}], path:"src/"})`|{{/if}}
|
|
61
|
-
<!-- markdownlint-enable MD055 MD056
|
|
61
|
+
<!-- markdownlint-enable MD055 MD056 -→
|
|
62
62
|
{{#if hasAstGrep}}- You **MUST** use `ast_grep` for structural code search instead of bash `grep`/`awk`/`perl` pipelines{{/if}}
|
|
63
63
|
{{#if hasAstEdit}}- You **MUST** use `ast_edit` for structural rewrites instead of bash `sed`/`awk`/`perl` pipelines{{/if}}
|
|
64
64
|
- You **MUST NOT** use Bash for these operations like read, grep, find, edit, write, where specialized tools exist.
|
|
@@ -3,7 +3,7 @@ Execute SOQL queries against Salesforce via sf CLI. Returns structured results a
|
|
|
3
3
|
<instruction>
|
|
4
4
|
Use for pipeline reporting, case management, account intelligence, and ad-hoc data queries.
|
|
5
5
|
|
|
6
|
-
Common query templates (substitute {userId} from
|
|
6
|
+
Common query templates (substitute {userId} from user profile — read `xcsh://user` to get identifiers.salesforceId):
|
|
7
7
|
|
|
8
8
|
Pipeline summary:
|
|
9
9
|
SELECT StageName, COUNT(Id) TotalDeals, SUM(Amount) TotalAmount FROM Opportunity WHERE IsClosed = false GROUP BY StageName ORDER BY SUM(Amount) DESC LIMIT 50
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
Salesforce onboarding wizard via sf CLI. Check installation, detect authentication, guide login
|
|
1
|
+
Salesforce onboarding wizard via sf CLI. Check installation, detect authentication, guide login.
|
|
2
2
|
|
|
3
3
|
<instruction>
|
|
4
|
-
Actions: "check" (verify sf installed), "status" (auth + default org
|
|
4
|
+
Actions: "check" (verify sf installed), "status" (auth + default org), "login" (detect auth and prompt user with login command), "list_orgs" (show all orgs), "set_default" (switch default org — requires org parameter with a valid alias).
|
|
5
5
|
|
|
6
|
-
When the user first asks about Salesforce, pipeline, cases, or accounts and no org is authenticated, run check, then status, then login if needed
|
|
6
|
+
When the user first asks about Salesforce, pipeline, cases, or accounts and no org is authenticated, run check, then status, then login if needed. The login action shows the user the exact command to run (sf org login web --set-default --alias SFDC for workstations, or echo "$SFDX_AUTH_URL" | sf org login sfdx-url --sfdx-url-stdin=- --set-default --alias f5 for containers).
|
|
7
7
|
|
|
8
8
|
The login action does NOT execute authentication. It detects state and tells the user what command to run.
|
|
9
|
-
|
|
9
|
+
User profile data is managed by the central profile builder via `xcsh://user?seed=true`.
|
|
10
10
|
</instruction>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SfOrg, SfQueryResult
|
|
1
|
+
import type { SfOrg, SfQueryResult } from "./types";
|
|
2
2
|
|
|
3
3
|
export function formatOrgTable(orgs: SfOrg[]): string {
|
|
4
4
|
if (orgs.length === 0) {
|
|
@@ -100,51 +100,3 @@ export function formatQueryResults(result: SfQueryResult): string {
|
|
|
100
100
|
|
|
101
101
|
return `${result.totalSize} records returned.\n\n${[header, divider, ...rows].join("\n")}`;
|
|
102
102
|
}
|
|
103
|
-
|
|
104
|
-
export function formatUserProfile(profile: SfUserProfile): string {
|
|
105
|
-
const lines: string[] = [];
|
|
106
|
-
|
|
107
|
-
lines.push(`**${profile.firstName} ${profile.lastName}** (${profile.username})`);
|
|
108
|
-
|
|
109
|
-
if (profile.title) {
|
|
110
|
-
lines.push(`Title: ${profile.title}`);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (profile.department) {
|
|
114
|
-
lines.push(`Department: ${profile.department}`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (profile.division) {
|
|
118
|
-
lines.push(`Division: ${profile.division}`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (profile.role) {
|
|
122
|
-
lines.push(`Role: ${profile.role}`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (profile.profile) {
|
|
126
|
-
lines.push(`Profile: ${profile.profile}`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (profile.aboutMe) {
|
|
130
|
-
lines.push(`About: ${profile.aboutMe}`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (profile.managerName) {
|
|
134
|
-
const managerLine = profile.managerEmail
|
|
135
|
-
? `Manager: ${profile.managerName} (${profile.managerEmail})`
|
|
136
|
-
: `Manager: ${profile.managerName}`;
|
|
137
|
-
lines.push(managerLine);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (profile.phone) {
|
|
141
|
-
lines.push(`Phone: ${profile.phone}`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const locationParts = [profile.city, profile.state, profile.country].filter(Boolean);
|
|
145
|
-
if (locationParts.length > 0) {
|
|
146
|
-
lines.push(`Location: ${locationParts.join(", ")}`);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return lines.join("\n");
|
|
150
|
-
}
|
package/src/tools/sf/types.ts
CHANGED
|
@@ -1,28 +1,3 @@
|
|
|
1
|
-
export interface SfUserProfile {
|
|
2
|
-
userId: string;
|
|
3
|
-
username: string;
|
|
4
|
-
firstName: string;
|
|
5
|
-
lastName: string;
|
|
6
|
-
email: string;
|
|
7
|
-
title?: string;
|
|
8
|
-
department?: string;
|
|
9
|
-
division?: string;
|
|
10
|
-
role?: string;
|
|
11
|
-
profile?: string;
|
|
12
|
-
aboutMe?: string;
|
|
13
|
-
companyName?: string;
|
|
14
|
-
managerId?: string;
|
|
15
|
-
managerName?: string;
|
|
16
|
-
managerEmail?: string;
|
|
17
|
-
phone?: string;
|
|
18
|
-
street?: string;
|
|
19
|
-
city?: string;
|
|
20
|
-
state?: string;
|
|
21
|
-
postalCode?: string;
|
|
22
|
-
country?: string;
|
|
23
|
-
fetchedAt: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
1
|
export interface SfOrg {
|
|
27
2
|
alias?: string;
|
|
28
3
|
username: string;
|
|
@@ -62,6 +37,4 @@ export interface SfRawResult {
|
|
|
62
37
|
|
|
63
38
|
export const SF_ORG_SAFE_FIELDS = ["username", "orgId", "instanceUrl", "connectedStatus", "alias"] as const;
|
|
64
39
|
|
|
65
|
-
export const USER_PROFILE_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}'`;
|
|
66
|
-
|
|
67
40
|
export const ORG_ALIAS_PATTERN = /^[a-zA-Z0-9._@-]+$/;
|
package/src/tools/sf.ts
CHANGED
|
@@ -6,16 +6,16 @@ import type {
|
|
|
6
6
|
} from "@f5xc-salesdemos/pi-agent-core";
|
|
7
7
|
import { $which, prompt } from "@f5xc-salesdemos/pi-utils";
|
|
8
8
|
import { type Static, Type } from "@sinclair/typebox";
|
|
9
|
+
import { loadProfile } from "../internal-urls/user-profile";
|
|
9
10
|
import sfOrgDisplayDescription from "../prompts/tools/sf-org-display.md" with { type: "text" };
|
|
10
11
|
import sfQueryDescription from "../prompts/tools/sf-query.md" with { type: "text" };
|
|
11
12
|
import sfSetupDescription from "../prompts/tools/sf-setup.md" with { type: "text" };
|
|
12
13
|
import type { ToolSession } from ".";
|
|
13
|
-
import { loadUserProfile, saveUserProfile } from "./sf/config";
|
|
14
14
|
import type { SfExecApi } from "./sf/exec";
|
|
15
15
|
import { execSfJson, execSfRaw } from "./sf/exec";
|
|
16
|
-
import { formatOrgDetail, formatOrgTable, formatQueryResults
|
|
17
|
-
import type { SfOrg, SfQueryResult, SfRawResult
|
|
18
|
-
import { ORG_ALIAS_PATTERN
|
|
16
|
+
import { formatOrgDetail, formatOrgTable, formatQueryResults } from "./sf/formatters";
|
|
17
|
+
import type { SfOrg, SfQueryResult, SfRawResult } from "./sf/types";
|
|
18
|
+
import { ORG_ALIAS_PATTERN } from "./sf/types";
|
|
19
19
|
|
|
20
20
|
function makeExecApi(cwd: string): SfExecApi {
|
|
21
21
|
return {
|
|
@@ -57,7 +57,6 @@ const sfSetupSchema = Type.Object({
|
|
|
57
57
|
Type.Literal("login"),
|
|
58
58
|
Type.Literal("list_orgs"),
|
|
59
59
|
Type.Literal("set_default"),
|
|
60
|
-
Type.Literal("profile"),
|
|
61
60
|
],
|
|
62
61
|
{ description: "Onboarding action to perform" },
|
|
63
62
|
),
|
|
@@ -84,7 +83,6 @@ type SfOrgDisplayInput = Static<typeof sfOrgDisplaySchema>;
|
|
|
84
83
|
interface SfToolDetails {
|
|
85
84
|
orgs?: SfOrg[];
|
|
86
85
|
queryResult?: SfQueryResult;
|
|
87
|
-
profile?: SfUserProfile;
|
|
88
86
|
}
|
|
89
87
|
|
|
90
88
|
function textResult(text: string, details?: SfToolDetails): AgentToolResult<SfToolDetails> {
|
|
@@ -125,19 +123,6 @@ export function collectAllOrgs(orgList: Record<string, unknown[]>): SfOrg[] {
|
|
|
125
123
|
});
|
|
126
124
|
}
|
|
127
125
|
|
|
128
|
-
function extractRelationshipField(
|
|
129
|
-
record: Record<string, unknown>,
|
|
130
|
-
relationship: string,
|
|
131
|
-
field: string,
|
|
132
|
-
): string | undefined {
|
|
133
|
-
const related = record[relationship];
|
|
134
|
-
if (related && typeof related === "object" && !Array.isArray(related)) {
|
|
135
|
-
const value = (related as Record<string, unknown>)[field];
|
|
136
|
-
if (value) return String(value);
|
|
137
|
-
}
|
|
138
|
-
return undefined;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
126
|
// ─── SfSetupTool ─────────────────────────────────────────────────────────
|
|
142
127
|
|
|
143
128
|
export class SfSetupTool implements AgentTool<typeof sfSetupSchema, SfToolDetails> {
|
|
@@ -179,9 +164,10 @@ export class SfSetupTool implements AgentTool<typeof sfSetupSchema, SfToolDetail
|
|
|
179
164
|
const allOrgs = collectAllOrgs(orgResult.result as Record<string, unknown[]>);
|
|
180
165
|
let output = formatOrgTable(allOrgs);
|
|
181
166
|
|
|
182
|
-
const
|
|
183
|
-
if (
|
|
184
|
-
|
|
167
|
+
const userProfile = await loadProfile();
|
|
168
|
+
if (userProfile.givenName || userProfile.familyName) {
|
|
169
|
+
const name = [userProfile.givenName, userProfile.familyName].filter(Boolean).join(" ");
|
|
170
|
+
output += `\n\nUser profile: **${name}** (${userProfile.email ?? "no email"})`;
|
|
185
171
|
}
|
|
186
172
|
|
|
187
173
|
return textResult(output, { orgs: allOrgs });
|
|
@@ -191,7 +177,7 @@ export class SfSetupTool implements AgentTool<typeof sfSetupSchema, SfToolDetail
|
|
|
191
177
|
const orgResult = await execSfJson(api, ["org", "list"], signal);
|
|
192
178
|
const allOrgs = collectAllOrgs(orgResult.result as Record<string, unknown[]>);
|
|
193
179
|
if (allOrgs.length > 0) {
|
|
194
|
-
return textResult("Already authenticated. Use '
|
|
180
|
+
return textResult("Already authenticated. Use 'status' action to see your orgs and profile.", {
|
|
195
181
|
orgs: allOrgs,
|
|
196
182
|
});
|
|
197
183
|
}
|
|
@@ -221,59 +207,6 @@ export class SfSetupTool implements AgentTool<typeof sfSetupSchema, SfToolDetail
|
|
|
221
207
|
await execSfRaw(api, ["config", "set", "target-org", params.org, "--global"], signal);
|
|
222
208
|
return textResult(`Default org set to: **${params.org}**`);
|
|
223
209
|
}
|
|
224
|
-
|
|
225
|
-
case "profile": {
|
|
226
|
-
// Step 1: Get the current user's username from org display
|
|
227
|
-
const orgInfo = await execSfJson(api, ["org", "display"], signal);
|
|
228
|
-
const orgResult = orgInfo.result as Record<string, unknown>;
|
|
229
|
-
const username = orgResult.username as string;
|
|
230
|
-
if (!username) {
|
|
231
|
-
return textResult("Could not determine username from org display. Ensure a default org is set.");
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Step 2: Build and run SOQL query for user profile
|
|
235
|
-
const soql = USER_PROFILE_SOQL.replace("{username}", username);
|
|
236
|
-
const queryResult = await execSfJson(api, ["data", "query", "--query", soql], signal, soql);
|
|
237
|
-
const queryData = queryResult.result as SfQueryResult<Record<string, unknown>>;
|
|
238
|
-
|
|
239
|
-
if (!queryData.records || queryData.records.length === 0) {
|
|
240
|
-
return textResult(`No user record found for username: ${username}`);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const record = queryData.records[0];
|
|
244
|
-
|
|
245
|
-
// Step 3: Map SOQL fields to SfUserProfile
|
|
246
|
-
const profile: SfUserProfile = {
|
|
247
|
-
userId: String(record.Id ?? ""),
|
|
248
|
-
username: String(record.Username ?? ""),
|
|
249
|
-
firstName: String(record.FirstName ?? ""),
|
|
250
|
-
lastName: String(record.LastName ?? ""),
|
|
251
|
-
email: String(record.Email ?? ""),
|
|
252
|
-
title: record.Title ? String(record.Title) : undefined,
|
|
253
|
-
department: record.Department ? String(record.Department) : undefined,
|
|
254
|
-
division: record.Division ? String(record.Division) : undefined,
|
|
255
|
-
companyName: record.CompanyName ? String(record.CompanyName) : undefined,
|
|
256
|
-
aboutMe: record.AboutMe ? String(record.AboutMe) : undefined,
|
|
257
|
-
managerId: record.ManagerId ? String(record.ManagerId) : undefined,
|
|
258
|
-
managerName: extractRelationshipField(record, "Manager", "Name"),
|
|
259
|
-
managerEmail: extractRelationshipField(record, "Manager", "Email"),
|
|
260
|
-
role: extractRelationshipField(record, "UserRole", "Name"),
|
|
261
|
-
profile: extractRelationshipField(record, "Profile", "Name"),
|
|
262
|
-
phone: record.Phone || record.MobilePhone ? String(record.Phone ?? record.MobilePhone) : undefined,
|
|
263
|
-
street: record.Street ? String(record.Street) : undefined,
|
|
264
|
-
city: record.City ? String(record.City) : undefined,
|
|
265
|
-
state: record.State ? String(record.State) : undefined,
|
|
266
|
-
postalCode: record.PostalCode ? String(record.PostalCode) : undefined,
|
|
267
|
-
country: record.Country ? String(record.Country) : undefined,
|
|
268
|
-
fetchedAt: new Date().toISOString(),
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
// Step 4: Cache the profile
|
|
272
|
-
await saveUserProfile(profile);
|
|
273
|
-
|
|
274
|
-
return textResult(formatUserProfile(profile), { profile });
|
|
275
|
-
}
|
|
276
|
-
|
|
277
210
|
default:
|
|
278
211
|
return textResult(`Unknown action: ${params.action}`);
|
|
279
212
|
}
|
package/src/tools/sf/config.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs/promises";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { isEnoent } from "@f5xc-salesdemos/pi-utils";
|
|
4
|
-
import type { SfUserProfile } from "./types";
|
|
5
|
-
|
|
6
|
-
export function getProfilePath(): string {
|
|
7
|
-
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
8
|
-
return path.join(home, ".xcsh", "sf-profile.json");
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export async function loadUserProfile(): Promise<SfUserProfile | null> {
|
|
12
|
-
try {
|
|
13
|
-
const raw = await fs.readFile(getProfilePath(), "utf8");
|
|
14
|
-
const profile = JSON.parse(raw) as SfUserProfile;
|
|
15
|
-
if (
|
|
16
|
-
!profile.userId ||
|
|
17
|
-
!profile.username ||
|
|
18
|
-
!profile.firstName ||
|
|
19
|
-
!profile.lastName ||
|
|
20
|
-
!profile.email ||
|
|
21
|
-
!profile.fetchedAt
|
|
22
|
-
) {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
return profile;
|
|
26
|
-
} catch (err) {
|
|
27
|
-
if (isEnoent(err)) return null;
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function saveUserProfile(profile: SfUserProfile): Promise<void> {
|
|
33
|
-
const profilePath = getProfilePath();
|
|
34
|
-
await fs.mkdir(path.dirname(profilePath), { recursive: true });
|
|
35
|
-
await fs.writeFile(profilePath, JSON.stringify(profile, null, 2), "utf8");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function clearUserProfile(): Promise<void> {
|
|
39
|
-
try {
|
|
40
|
-
await fs.unlink(getProfilePath());
|
|
41
|
-
} catch (err) {
|
|
42
|
-
if (!isEnoent(err)) throw err;
|
|
43
|
-
}
|
|
44
|
-
}
|