@f5xc-salesdemos/xcsh 18.66.2 → 18.68.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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "18.
|
|
4
|
+
"version": "18.68.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.68.0",
|
|
52
|
+
"@f5xc-salesdemos/pi-agent-core": "18.68.0",
|
|
53
|
+
"@f5xc-salesdemos/pi-ai": "18.68.0",
|
|
54
|
+
"@f5xc-salesdemos/pi-natives": "18.68.0",
|
|
55
|
+
"@f5xc-salesdemos/pi-tui": "18.68.0",
|
|
56
|
+
"@f5xc-salesdemos/pi-utils": "18.68.0",
|
|
57
57
|
"@sinclair/typebox": "^0.34",
|
|
58
58
|
"@xterm/headless": "^6.0",
|
|
59
59
|
"ajv": "^8.18",
|
|
@@ -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.68.0",
|
|
21
|
+
"commit": "27ddc863792ce7bba1ab75066abeca95cafca4d4",
|
|
22
|
+
"shortCommit": "27ddc86",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v18.
|
|
25
|
-
"commitDate": "2026-05-
|
|
26
|
-
"buildDate": "2026-05-
|
|
24
|
+
"tag": "v18.68.0",
|
|
25
|
+
"commitDate": "2026-05-18T17:51:04Z",
|
|
26
|
+
"buildDate": "2026-05-18T18:22:44.227Z",
|
|
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/27ddc863792ce7bba1ab75066abeca95cafca4d4",
|
|
32
|
+
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.68.0"
|
|
33
33
|
};
|
|
@@ -625,3 +625,44 @@ export function formatParseErrors(errors: string[]): string[] {
|
|
|
625
625
|
: "Parse issues:";
|
|
626
626
|
return [header, ...capped.map(err => `- ${err}`)];
|
|
627
627
|
}
|
|
628
|
+
|
|
629
|
+
// =============================================================================
|
|
630
|
+
// JSON / Display Utilities (shared with tool renderers)
|
|
631
|
+
// =============================================================================
|
|
632
|
+
|
|
633
|
+
export function stripEmpty(obj: unknown): unknown {
|
|
634
|
+
if (Array.isArray(obj)) return obj.map(stripEmpty).filter(v => v != null);
|
|
635
|
+
if (obj && typeof obj === "object") {
|
|
636
|
+
const entries = Object.entries(obj as Record<string, unknown>);
|
|
637
|
+
if (entries.length === 0) return obj;
|
|
638
|
+
const out: Record<string, unknown> = {};
|
|
639
|
+
for (const [k, v] of entries) {
|
|
640
|
+
if (v == null || v === "" || (Array.isArray(v) && v.length === 0)) continue;
|
|
641
|
+
const cleaned = stripEmpty(v);
|
|
642
|
+
if (cleaned != null) out[k] = cleaned;
|
|
643
|
+
}
|
|
644
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
645
|
+
}
|
|
646
|
+
return obj;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export function formatTimestamp(iso: string): string {
|
|
650
|
+
return iso.replace("T", " ").replace(/:\d{2}(\.\d+)?Z$/, " UTC");
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
export function addSection(
|
|
654
|
+
sections: Array<{ label?: string; lines: string[] }>,
|
|
655
|
+
label: string,
|
|
656
|
+
lines: string[],
|
|
657
|
+
theme: Theme,
|
|
658
|
+
maxLines?: number,
|
|
659
|
+
): void {
|
|
660
|
+
const titled = theme.fg("toolTitle", label);
|
|
661
|
+
if (maxLines && lines.length > maxLines) {
|
|
662
|
+
const truncated = lines.slice(0, maxLines);
|
|
663
|
+
truncated.push(theme.fg("dim", `… ${lines.length - maxLines} more lines`));
|
|
664
|
+
sections.push({ label: titled, lines: truncated });
|
|
665
|
+
} else {
|
|
666
|
+
sections.push({ label: titled, lines });
|
|
667
|
+
}
|
|
668
|
+
}
|
package/src/tools/renderers.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { notebookToolRenderer } from "./notebook";
|
|
|
24
24
|
import { pythonToolRenderer } from "./python";
|
|
25
25
|
import { resolveToolRenderer } from "./resolve";
|
|
26
26
|
import { searchToolBm25Renderer } from "./search-tool-bm25";
|
|
27
|
+
import { sfToolRenderer } from "./sf-renderer";
|
|
27
28
|
import { sshToolRenderer } from "./ssh";
|
|
28
29
|
import { todoWriteToolRenderer } from "./todo-write";
|
|
29
30
|
import { writeToolRenderer } from "./write";
|
|
@@ -70,4 +71,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
|
|
|
70
71
|
web_search: webSearchToolRenderer as ToolRenderer,
|
|
71
72
|
write: writeToolRenderer as ToolRenderer,
|
|
72
73
|
xcsh_api: xcshApiToolRenderer as ToolRenderer,
|
|
74
|
+
sf_setup: sfToolRenderer as ToolRenderer,
|
|
75
|
+
sf_query: sfToolRenderer as ToolRenderer,
|
|
76
|
+
sf_org_display: sfToolRenderer as ToolRenderer,
|
|
73
77
|
};
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/** TUI renderer for Salesforce tools — rich visual output at full parity with XC-API. */
|
|
2
|
+
import type { Component } from "@f5xc-salesdemos/pi-tui";
|
|
3
|
+
import { Text } from "@f5xc-salesdemos/pi-tui";
|
|
4
|
+
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
5
|
+
import type { Theme, ThemeColor } from "../modes/theme/theme";
|
|
6
|
+
import { CachedOutputBlock, F5_TOOL_BORDER_COLOR, renderStatusLine } from "../tui";
|
|
7
|
+
import { addSection, formatErrorMessage, replaceTabs } from "./render-utils";
|
|
8
|
+
import type { SfErrorType, SfToolDetails } from "./sf";
|
|
9
|
+
import { flattenRecord } from "./sf/formatters";
|
|
10
|
+
import type { SfOrg, SfQueryResult } from "./sf/types";
|
|
11
|
+
|
|
12
|
+
const TOOL_TITLE = "Salesforce";
|
|
13
|
+
const MAX_COL_WIDTH = 30;
|
|
14
|
+
|
|
15
|
+
type SfRenderArgs = {
|
|
16
|
+
action?: string;
|
|
17
|
+
query?: string;
|
|
18
|
+
target_org?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const TOOL_ACTION_COLORS: Partial<Record<string, ThemeColor>> = {
|
|
22
|
+
sf_setup: "chromeAccent",
|
|
23
|
+
sf_query: "contentAccent",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const ERROR_GUIDANCE: Record<SfErrorType, string> = {
|
|
27
|
+
auth_required: "Authenticate with: sf org login web --set-default --alias SFDC",
|
|
28
|
+
session_expired: "Re-authenticate: sf org login web --set-default\nThen run sf_setup action 'status' to confirm",
|
|
29
|
+
no_default_org: "Run sf_setup with action set_default to choose a default org",
|
|
30
|
+
invalid_query:
|
|
31
|
+
"Check field names and object types. Use SELECT ... FROM EntityDefinition to discover available objects",
|
|
32
|
+
exec_error: "Check sf CLI is installed and configured correctly",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function orgStatusColor(status: string): ThemeColor {
|
|
36
|
+
const lower = status.toLowerCase();
|
|
37
|
+
if (lower === "connected") return "success";
|
|
38
|
+
if (lower.includes("expired")) return "error";
|
|
39
|
+
return "warning";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function truncateCell(value: string, maxWidth: number): string {
|
|
43
|
+
if (value.length <= maxWidth) return value;
|
|
44
|
+
return `${value.slice(0, maxWidth - 1)}…`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildOrgRows(orgs: SfOrg[], uiTheme: Theme): string[] {
|
|
48
|
+
return orgs.map(org => {
|
|
49
|
+
const defaultBadge = org.isDefault ? ` ${uiTheme.fg("chromeAccent", "(default)")}` : "";
|
|
50
|
+
const aliasText = org.alias
|
|
51
|
+
? uiTheme.fg("toolOutput", org.alias) + defaultBadge
|
|
52
|
+
: uiTheme.fg("dim", "(none)") + defaultBadge;
|
|
53
|
+
const username = uiTheme.fg("dim", org.username);
|
|
54
|
+
const status = uiTheme.fg(orgStatusColor(org.connectedStatus), org.connectedStatus);
|
|
55
|
+
const sandboxBadge = org.isSandbox ? ` ${uiTheme.fg("warning", "[sandbox]")}` : "";
|
|
56
|
+
return ` ${aliasText} ${username} ${status}${sandboxBadge}`;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildQueryTable(queryResult: SfQueryResult, uiTheme: Theme): string[] {
|
|
61
|
+
const records = queryResult.records as Record<string, unknown>[];
|
|
62
|
+
if (records.length === 0) return [uiTheme.fg("dim", " No records found.")];
|
|
63
|
+
|
|
64
|
+
const flatRecords = records.map(r => flattenRecord(r));
|
|
65
|
+
const allColumns = Array.from(
|
|
66
|
+
flatRecords.reduce((cols, record) => {
|
|
67
|
+
for (const key of Object.keys(record)) cols.add(key);
|
|
68
|
+
return cols;
|
|
69
|
+
}, new Set<string>()),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const colWidths = allColumns.map(col => {
|
|
73
|
+
const maxData = flatRecords.reduce((max, rec) => {
|
|
74
|
+
const val = rec[col];
|
|
75
|
+
return Math.max(max, val == null ? 0 : String(val).replace(/[\n\r\t]+/g, " ").length);
|
|
76
|
+
}, 0);
|
|
77
|
+
return Math.min(MAX_COL_WIDTH, Math.max(col.length, maxData));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const lines: string[] = [];
|
|
81
|
+
|
|
82
|
+
// Header
|
|
83
|
+
const headerCells = allColumns.map((col, i) => uiTheme.fg("toolTitle", col.padEnd(colWidths[i]!)));
|
|
84
|
+
lines.push(` ${headerCells.join(" ")}`);
|
|
85
|
+
|
|
86
|
+
// Separator
|
|
87
|
+
const sepCells = colWidths.map(w => uiTheme.fg("dim", "─".repeat(w)));
|
|
88
|
+
lines.push(` ${sepCells.join(" ")}`);
|
|
89
|
+
|
|
90
|
+
// Rows
|
|
91
|
+
for (const rec of flatRecords) {
|
|
92
|
+
const cells = allColumns.map((col, i) => {
|
|
93
|
+
const val = rec[col];
|
|
94
|
+
const raw = val == null ? "" : String(val).replace(/[\n\r\t]+/g, " ");
|
|
95
|
+
const cell = truncateCell(raw, colWidths[i]!).padEnd(colWidths[i]!);
|
|
96
|
+
return val == null || raw === "" ? uiTheme.fg("dim", cell) : uiTheme.fg("toolOutput", cell);
|
|
97
|
+
});
|
|
98
|
+
lines.push(` ${cells.join(" ")}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return lines;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildOrgKV(org: SfOrg, uiTheme: Theme): string[] {
|
|
105
|
+
const line = (label: string, value: string, valueColor: ThemeColor = "toolOutput") =>
|
|
106
|
+
` ${uiTheme.fg("dim", label.padEnd(10))}${uiTheme.fg(valueColor, value)}`;
|
|
107
|
+
|
|
108
|
+
const lines: string[] = [];
|
|
109
|
+
if (org.alias) lines.push(line("alias:", org.alias));
|
|
110
|
+
lines.push(line("username:", org.username));
|
|
111
|
+
lines.push(line("org id:", org.orgId));
|
|
112
|
+
lines.push(line("instance:", org.instanceUrl));
|
|
113
|
+
lines.push(line("status:", org.connectedStatus, orgStatusColor(org.connectedStatus)));
|
|
114
|
+
if (org.isSandbox) lines.push(line("type:", "Sandbox", "warning"));
|
|
115
|
+
if (org.isDefault) lines.push(line("default:", "yes", "chromeAccent"));
|
|
116
|
+
return lines;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const sfToolRenderer = {
|
|
120
|
+
renderCall(args: SfRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
121
|
+
const action = args.action ?? (args.query !== undefined ? "query" : "org");
|
|
122
|
+
const text = renderStatusLine(
|
|
123
|
+
{
|
|
124
|
+
icon: "pending",
|
|
125
|
+
title: TOOL_TITLE,
|
|
126
|
+
badge: { label: action, color: args.query !== undefined ? "contentAccent" : "chromeAccent" },
|
|
127
|
+
},
|
|
128
|
+
uiTheme,
|
|
129
|
+
);
|
|
130
|
+
return new Text(text, 0, 0);
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
renderResult(
|
|
134
|
+
result: { content: Array<{ type: string; text?: string }>; details?: SfToolDetails; isError?: boolean },
|
|
135
|
+
options: RenderResultOptions,
|
|
136
|
+
uiTheme: Theme,
|
|
137
|
+
_args?: SfRenderArgs,
|
|
138
|
+
): Component {
|
|
139
|
+
const details = result.details;
|
|
140
|
+
const isError = result.isError === true;
|
|
141
|
+
|
|
142
|
+
// Fallback: error without structured details
|
|
143
|
+
if (isError && !details) {
|
|
144
|
+
const errorText = result.content?.find(c => c.type === "text")?.text;
|
|
145
|
+
return new Text(formatErrorMessage(errorText, uiTheme), 0, 0);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const tool = details?.tool;
|
|
149
|
+
const action = details?.action;
|
|
150
|
+
const errorType = details?.errorType;
|
|
151
|
+
const sections: Array<{ label?: string; lines: string[] }> = [];
|
|
152
|
+
|
|
153
|
+
// --- Error path ---
|
|
154
|
+
if (isError) {
|
|
155
|
+
const errorText = result.content?.find(c => c.type === "text")?.text ?? "Unknown error";
|
|
156
|
+
addSection(sections, "Error", [uiTheme.fg("error", errorText)], uiTheme);
|
|
157
|
+
|
|
158
|
+
const badgeLabel = errorType ?? "error";
|
|
159
|
+
if (errorType) {
|
|
160
|
+
const guidance = ERROR_GUIDANCE[errorType];
|
|
161
|
+
const guidanceLines = guidance.split("\n").map(l => uiTheme.fg("warning", l));
|
|
162
|
+
addSection(sections, "Guidance", guidanceLines, uiTheme);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const header = renderStatusLine(
|
|
166
|
+
{
|
|
167
|
+
title: TOOL_TITLE,
|
|
168
|
+
titleColor: "contentAccent",
|
|
169
|
+
badge: { label: badgeLabel, color: "error" },
|
|
170
|
+
},
|
|
171
|
+
uiTheme,
|
|
172
|
+
);
|
|
173
|
+
const outputBlock = new CachedOutputBlock();
|
|
174
|
+
return {
|
|
175
|
+
render(width: number): string[] {
|
|
176
|
+
return outputBlock.render({ header, state: "error", sections, width }, uiTheme);
|
|
177
|
+
},
|
|
178
|
+
invalidate() {
|
|
179
|
+
outputBlock.invalidate();
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// --- Success path ---
|
|
185
|
+
let badgeLabel = action ?? tool?.replace("sf_", "") ?? "sf";
|
|
186
|
+
const badgeColor: ThemeColor = tool ? (TOOL_ACTION_COLORS[tool] ?? "muted") : "muted";
|
|
187
|
+
const meta: string[] = [];
|
|
188
|
+
|
|
189
|
+
if (tool === "sf_setup") {
|
|
190
|
+
const orgs = details?.orgs;
|
|
191
|
+
if ((action === "status" || action === "list_orgs") && orgs !== undefined) {
|
|
192
|
+
const count = orgs.length;
|
|
193
|
+
meta.push(uiTheme.fg("dim", `${count} org${count !== 1 ? "s" : ""}`));
|
|
194
|
+
if (count === 0) {
|
|
195
|
+
addSection(sections, "Orgs", [uiTheme.fg("dim", "No authenticated orgs found.")], uiTheme);
|
|
196
|
+
} else {
|
|
197
|
+
addSection(sections, "Orgs", buildOrgRows(orgs, uiTheme), uiTheme);
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
const text = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
201
|
+
addSection(
|
|
202
|
+
sections,
|
|
203
|
+
"Result",
|
|
204
|
+
text.split("\n").map(line => replaceTabs(uiTheme.fg("toolOutput", line))),
|
|
205
|
+
uiTheme,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
} else if (tool === "sf_query") {
|
|
209
|
+
const queryResult = details?.queryResult;
|
|
210
|
+
if (queryResult) {
|
|
211
|
+
const count = queryResult.totalSize;
|
|
212
|
+
badgeLabel = "query";
|
|
213
|
+
meta.push(uiTheme.fg("dim", `${count} record${count !== 1 ? "s" : ""}`));
|
|
214
|
+
addSection(
|
|
215
|
+
sections,
|
|
216
|
+
`Results (${count} record${count !== 1 ? "s" : ""})`,
|
|
217
|
+
buildQueryTable(queryResult, uiTheme),
|
|
218
|
+
uiTheme,
|
|
219
|
+
);
|
|
220
|
+
if (!queryResult.done) {
|
|
221
|
+
addSection(
|
|
222
|
+
sections,
|
|
223
|
+
"Warning",
|
|
224
|
+
[uiTheme.fg("warning", "Results are incomplete. Use sf data export bulk for the full dataset.")],
|
|
225
|
+
uiTheme,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
const text = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
230
|
+
addSection(sections, "Result", [uiTheme.fg("toolOutput", text)], uiTheme);
|
|
231
|
+
}
|
|
232
|
+
} else if (tool === "sf_org_display") {
|
|
233
|
+
const org = details?.orgs?.[0];
|
|
234
|
+
if (org) {
|
|
235
|
+
badgeLabel = "org";
|
|
236
|
+
meta.push(uiTheme.fg("muted", org.alias ?? org.username));
|
|
237
|
+
meta.push(uiTheme.fg(orgStatusColor(org.connectedStatus), org.connectedStatus));
|
|
238
|
+
addSection(sections, "Summary", buildOrgKV(org, uiTheme), uiTheme);
|
|
239
|
+
} else {
|
|
240
|
+
const text = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
241
|
+
addSection(sections, "Result", [uiTheme.fg("toolOutput", text)], uiTheme);
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
const text = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
245
|
+
addSection(sections, "Result", [uiTheme.fg("toolOutput", text)], uiTheme);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const header = renderStatusLine(
|
|
249
|
+
{
|
|
250
|
+
title: TOOL_TITLE,
|
|
251
|
+
titleColor: "contentAccent",
|
|
252
|
+
badge: { label: badgeLabel, color: badgeColor },
|
|
253
|
+
meta: meta.length > 0 ? meta : undefined,
|
|
254
|
+
},
|
|
255
|
+
uiTheme,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const outputBlock = new CachedOutputBlock();
|
|
259
|
+
return {
|
|
260
|
+
render(width: number): string[] {
|
|
261
|
+
const state = options.isPartial ? "pending" : "success";
|
|
262
|
+
return outputBlock.render({ header, state, sections, width, borderColor: F5_TOOL_BORDER_COLOR }, uiTheme);
|
|
263
|
+
},
|
|
264
|
+
invalidate() {
|
|
265
|
+
outputBlock.invalidate();
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
mergeCallAndResult: true,
|
|
271
|
+
inline: true,
|
|
272
|
+
};
|
package/src/tools/sf.ts
CHANGED
|
@@ -12,7 +12,14 @@ import sfQueryDescription from "../prompts/tools/sf-query.md" with { type: "text
|
|
|
12
12
|
import sfSetupDescription from "../prompts/tools/sf-setup.md" with { type: "text" };
|
|
13
13
|
import type { ToolSession } from ".";
|
|
14
14
|
import type { SfExecApi } from "./sf/exec";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
execSfJson,
|
|
17
|
+
execSfRaw,
|
|
18
|
+
SfAuthError,
|
|
19
|
+
SfNoDefaultOrgError,
|
|
20
|
+
SfQueryError,
|
|
21
|
+
SfSessionExpiredError,
|
|
22
|
+
} from "./sf/exec";
|
|
16
23
|
import { formatOrgDetail, formatOrgTable, formatQueryResults } from "./sf/formatters";
|
|
17
24
|
import type { SfOrg, SfQueryResult, SfRawResult } from "./sf/types";
|
|
18
25
|
import { ORG_ALIAS_PATTERN } from "./sf/types";
|
|
@@ -80,15 +87,34 @@ type SfSetupInput = Static<typeof sfSetupSchema>;
|
|
|
80
87
|
type SfQueryInput = Static<typeof sfQuerySchema>;
|
|
81
88
|
type SfOrgDisplayInput = Static<typeof sfOrgDisplaySchema>;
|
|
82
89
|
|
|
83
|
-
|
|
90
|
+
export type SfErrorType = "auth_required" | "session_expired" | "no_default_org" | "invalid_query" | "exec_error";
|
|
91
|
+
|
|
92
|
+
export interface SfToolDetails {
|
|
93
|
+
tool: "sf_setup" | "sf_query" | "sf_org_display";
|
|
94
|
+
action?: string;
|
|
84
95
|
orgs?: SfOrg[];
|
|
85
96
|
queryResult?: SfQueryResult;
|
|
97
|
+
errorType?: SfErrorType;
|
|
86
98
|
}
|
|
87
99
|
|
|
88
|
-
|
|
100
|
+
type SfResult = AgentToolResult<SfToolDetails> & { isError?: boolean };
|
|
101
|
+
|
|
102
|
+
function textResult(text: string, details: SfToolDetails): SfResult {
|
|
89
103
|
return { content: [{ type: "text", text }], details };
|
|
90
104
|
}
|
|
91
105
|
|
|
106
|
+
function errorResult(text: string, details: SfToolDetails): SfResult {
|
|
107
|
+
return { content: [{ type: "text", text }], isError: true, details };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function detectErrorType(err: unknown): SfErrorType {
|
|
111
|
+
if (err instanceof SfAuthError) return "auth_required";
|
|
112
|
+
if (err instanceof SfSessionExpiredError) return "session_expired";
|
|
113
|
+
if (err instanceof SfNoDefaultOrgError) return "no_default_org";
|
|
114
|
+
if (err instanceof SfQueryError) return "invalid_query";
|
|
115
|
+
return "exec_error";
|
|
116
|
+
}
|
|
117
|
+
|
|
92
118
|
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
93
119
|
|
|
94
120
|
export function normalizeOrg(raw: Record<string, unknown>): SfOrg {
|
|
@@ -150,65 +176,76 @@ export class SfSetupTool implements AgentTool<typeof sfSetupSchema, SfToolDetail
|
|
|
150
176
|
signal?: AbortSignal,
|
|
151
177
|
_onUpdate?: AgentToolUpdateCallback<SfToolDetails>,
|
|
152
178
|
_context?: AgentToolContext,
|
|
153
|
-
): Promise<
|
|
179
|
+
): Promise<SfResult> {
|
|
154
180
|
const api = this.#testApi ?? makeExecApi(this.session.cwd);
|
|
181
|
+
const base = { tool: "sf_setup" as const, action: params.action };
|
|
155
182
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
case "status": {
|
|
163
|
-
const orgResult = await execSfJson(api, ["org", "list"], signal);
|
|
164
|
-
const allOrgs = collectAllOrgs(orgResult.result as Record<string, unknown[]>);
|
|
165
|
-
let output = formatOrgTable(allOrgs);
|
|
166
|
-
|
|
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"})`;
|
|
183
|
+
try {
|
|
184
|
+
switch (params.action) {
|
|
185
|
+
case "check": {
|
|
186
|
+
const result = await execSfRaw(api, ["--version"], signal);
|
|
187
|
+
return textResult(`sf is installed: ${result.stdout}`, base);
|
|
171
188
|
}
|
|
172
189
|
|
|
173
|
-
|
|
174
|
-
|
|
190
|
+
case "status": {
|
|
191
|
+
const orgResult = await execSfJson(api, ["org", "list"], signal);
|
|
192
|
+
const allOrgs = collectAllOrgs(orgResult.result as Record<string, unknown[]>);
|
|
193
|
+
let output = formatOrgTable(allOrgs);
|
|
175
194
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
orgs: allOrgs,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
return textResult(
|
|
185
|
-
"No authenticated orgs found.\n\nRun one of these commands to authenticate:\n" +
|
|
186
|
-
"- **Workstation**: `sf org login web --set-default --alias SFDC`\n" +
|
|
187
|
-
'- **Container**: `echo "$SFDX_AUTH_URL" | sf org login sfdx-url --sfdx-url-stdin=- --set-default --alias f5`\n\n' +
|
|
188
|
-
"After authenticating, call sf_setup with action 'status' to confirm.",
|
|
189
|
-
);
|
|
190
|
-
}
|
|
195
|
+
const userProfile = await loadProfile();
|
|
196
|
+
if (userProfile.givenName || userProfile.familyName) {
|
|
197
|
+
const name = [userProfile.givenName, userProfile.familyName].filter(Boolean).join(" ");
|
|
198
|
+
output += `\n\nUser profile: **${name}** (${userProfile.email ?? "no email"})`;
|
|
199
|
+
}
|
|
191
200
|
|
|
192
|
-
|
|
193
|
-
const orgResult = await execSfJson(api, ["org", "list"], signal);
|
|
194
|
-
const allOrgs = collectAllOrgs(orgResult.result as Record<string, unknown[]>);
|
|
195
|
-
return textResult(formatOrgTable(allOrgs), { orgs: allOrgs });
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
case "set_default": {
|
|
199
|
-
if (!params.org) {
|
|
200
|
-
return textResult("Error: org parameter is required for set_default action.");
|
|
201
|
+
return textResult(output, { ...base, orgs: allOrgs });
|
|
201
202
|
}
|
|
202
|
-
|
|
203
|
+
|
|
204
|
+
case "login": {
|
|
205
|
+
const orgResult = await execSfJson(api, ["org", "list"], signal);
|
|
206
|
+
const allOrgs = collectAllOrgs(orgResult.result as Record<string, unknown[]>);
|
|
207
|
+
if (allOrgs.length > 0) {
|
|
208
|
+
return textResult("Already authenticated. Use 'status' action to see your orgs and profile.", {
|
|
209
|
+
...base,
|
|
210
|
+
orgs: allOrgs,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
203
213
|
return textResult(
|
|
204
|
-
|
|
214
|
+
"No authenticated orgs found.\n\nRun one of these commands to authenticate:\n" +
|
|
215
|
+
"- **Workstation**: `sf org login web --set-default --alias SFDC`\n" +
|
|
216
|
+
'- **Container**: `echo "$SFDX_AUTH_URL" | sf org login sfdx-url --sfdx-url-stdin=- --set-default --alias f5`\n\n' +
|
|
217
|
+
"After authenticating, call sf_setup with action 'status' to confirm.",
|
|
218
|
+
base,
|
|
205
219
|
);
|
|
206
220
|
}
|
|
207
|
-
|
|
208
|
-
|
|
221
|
+
|
|
222
|
+
case "list_orgs": {
|
|
223
|
+
const orgResult = await execSfJson(api, ["org", "list"], signal);
|
|
224
|
+
const allOrgs = collectAllOrgs(orgResult.result as Record<string, unknown[]>);
|
|
225
|
+
return textResult(formatOrgTable(allOrgs), { ...base, orgs: allOrgs });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
case "set_default": {
|
|
229
|
+
if (!params.org) {
|
|
230
|
+
return errorResult("Error: org parameter is required for set_default action.", base);
|
|
231
|
+
}
|
|
232
|
+
if (!ORG_ALIAS_PATTERN.test(params.org)) {
|
|
233
|
+
return errorResult(
|
|
234
|
+
`Error: invalid org alias "${params.org}". Only alphanumeric characters, dots, underscores, hyphens, and @ are allowed.`,
|
|
235
|
+
base,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
await execSfRaw(api, ["config", "set", "target-org", params.org, "--global"], signal);
|
|
239
|
+
return textResult(`Default org set to: **${params.org}**`, base);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
default:
|
|
243
|
+
return textResult(`Unknown action: ${params.action}`, base);
|
|
209
244
|
}
|
|
210
|
-
|
|
211
|
-
|
|
245
|
+
} catch (err) {
|
|
246
|
+
const errorType = detectErrorType(err);
|
|
247
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
248
|
+
return errorResult(message, { ...base, errorType });
|
|
212
249
|
}
|
|
213
250
|
}
|
|
214
251
|
}
|
|
@@ -240,41 +277,42 @@ export class SfQueryTool implements AgentTool<typeof sfQuerySchema, SfToolDetail
|
|
|
240
277
|
signal?: AbortSignal,
|
|
241
278
|
_onUpdate?: AgentToolUpdateCallback<SfToolDetails>,
|
|
242
279
|
_context?: AgentToolContext,
|
|
243
|
-
): Promise<
|
|
280
|
+
): Promise<SfResult> {
|
|
244
281
|
const api = this.#testApi ?? makeExecApi(this.session.cwd);
|
|
282
|
+
const base = { tool: "sf_query" as const, action: "query" };
|
|
245
283
|
|
|
246
284
|
if (params.target_org && !ORG_ALIAS_PATTERN.test(params.target_org)) {
|
|
247
|
-
return
|
|
285
|
+
return errorResult(
|
|
248
286
|
`Error: invalid org alias "${params.target_org}". Only alphanumeric characters, dots, underscores, hyphens, and @ are allowed.`,
|
|
287
|
+
base,
|
|
249
288
|
);
|
|
250
289
|
}
|
|
251
290
|
|
|
252
291
|
const args = ["data", "query", "--query", params.query];
|
|
253
|
-
if (params.target_org)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const queryResult: SfQueryResult = {
|
|
267
|
-
totalSize: queryData.totalSize ?? 0,
|
|
268
|
-
done: queryData.done ?? true,
|
|
269
|
-
records: queryData.records ?? [],
|
|
270
|
-
};
|
|
292
|
+
if (params.target_org) args.push("--target-org", params.target_org);
|
|
293
|
+
if (params.use_tooling_api) args.push("--use-tooling-api");
|
|
294
|
+
if (params.all_rows) args.push("--all-rows");
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const result = await execSfJson(api, args, signal, params.query);
|
|
298
|
+
const queryData = result.result as SfQueryResult<Record<string, unknown>>;
|
|
299
|
+
const queryResult: SfQueryResult = {
|
|
300
|
+
totalSize: queryData.totalSize ?? 0,
|
|
301
|
+
done: queryData.done ?? true,
|
|
302
|
+
records: queryData.records ?? [],
|
|
303
|
+
};
|
|
271
304
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
305
|
+
let output = formatQueryResults(queryResult);
|
|
306
|
+
if (!queryResult.done) {
|
|
307
|
+
output +=
|
|
308
|
+
"\n\n**Warning**: Results are incomplete. The query returned more records than the API limit. Use `sf data export bulk` for the full dataset.";
|
|
309
|
+
}
|
|
310
|
+
return textResult(output, { ...base, queryResult });
|
|
311
|
+
} catch (err) {
|
|
312
|
+
const errorType = detectErrorType(err);
|
|
313
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
314
|
+
return errorResult(message, { ...base, errorType });
|
|
276
315
|
}
|
|
277
|
-
return textResult(output, { queryResult });
|
|
278
316
|
}
|
|
279
317
|
}
|
|
280
318
|
|
|
@@ -305,34 +343,40 @@ export class SfOrgDisplayTool implements AgentTool<typeof sfOrgDisplaySchema, Sf
|
|
|
305
343
|
signal?: AbortSignal,
|
|
306
344
|
_onUpdate?: AgentToolUpdateCallback<SfToolDetails>,
|
|
307
345
|
_context?: AgentToolContext,
|
|
308
|
-
): Promise<
|
|
346
|
+
): Promise<SfResult> {
|
|
309
347
|
const api = this.#testApi ?? makeExecApi(this.session.cwd);
|
|
348
|
+
const base = { tool: "sf_org_display" as const };
|
|
310
349
|
|
|
311
350
|
if (params.target_org && !ORG_ALIAS_PATTERN.test(params.target_org)) {
|
|
312
|
-
return
|
|
351
|
+
return errorResult(
|
|
313
352
|
`Error: invalid org alias "${params.target_org}". Only alphanumeric characters, dots, underscores, hyphens, and @ are allowed.`,
|
|
353
|
+
base,
|
|
314
354
|
);
|
|
315
355
|
}
|
|
316
356
|
|
|
317
357
|
const args = ["org", "display"];
|
|
318
|
-
if (params.target_org)
|
|
319
|
-
|
|
320
|
-
|
|
358
|
+
if (params.target_org) args.push("--target-org", params.target_org);
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
const result = await execSfJson(api, args, signal);
|
|
362
|
+
const raw = result.result as Record<string, unknown>;
|
|
363
|
+
|
|
364
|
+
// SECURITY: only extract whitelisted fields
|
|
365
|
+
const org: SfOrg = {
|
|
366
|
+
username: String(raw.username ?? ""),
|
|
367
|
+
orgId: String(raw.id ?? raw.orgId ?? ""),
|
|
368
|
+
instanceUrl: String(raw.instanceUrl ?? ""),
|
|
369
|
+
connectedStatus: String(raw.connectedStatus ?? "Connected"),
|
|
370
|
+
alias: raw.alias ? String(raw.alias) : undefined,
|
|
371
|
+
isDefault: false,
|
|
372
|
+
isSandbox: Boolean(raw.isSandbox ?? false),
|
|
373
|
+
};
|
|
321
374
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
orgId: String(raw.id ?? raw.orgId ?? ""),
|
|
329
|
-
instanceUrl: String(raw.instanceUrl ?? ""),
|
|
330
|
-
connectedStatus: String(raw.connectedStatus ?? "Connected"),
|
|
331
|
-
alias: raw.alias ? String(raw.alias) : undefined,
|
|
332
|
-
isDefault: false,
|
|
333
|
-
isSandbox: Boolean(raw.isSandbox ?? false),
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
return textResult(formatOrgDetail(org), { orgs: [org] });
|
|
375
|
+
return textResult(formatOrgDetail(org), { ...base, orgs: [org] });
|
|
376
|
+
} catch (err) {
|
|
377
|
+
const errorType = detectErrorType(err);
|
|
378
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
379
|
+
return errorResult(message, { ...base, errorType });
|
|
380
|
+
}
|
|
337
381
|
}
|
|
338
382
|
}
|
|
@@ -5,7 +5,13 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
|
5
5
|
import type { Theme, ThemeColor } from "../modes/theme/theme";
|
|
6
6
|
import { highlightCode } from "../modes/theme/theme";
|
|
7
7
|
import { CachedOutputBlock, F5_TOOL_BORDER_COLOR, renderStatusLine } from "../tui";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
formatErrorMessage,
|
|
10
|
+
formatTimestamp,
|
|
11
|
+
addSection as pushSection,
|
|
12
|
+
replaceTabs,
|
|
13
|
+
stripEmpty,
|
|
14
|
+
} from "./render-utils";
|
|
9
15
|
import type { XcshApiToolDetails } from "./xcsh-api";
|
|
10
16
|
|
|
11
17
|
const TOOL_TITLE = "XC-API";
|
|
@@ -31,30 +37,6 @@ function statusColor(status: number): ThemeColor {
|
|
|
31
37
|
return status < 300 ? "success" : status < 400 ? "warning" : "error";
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
/**
|
|
35
|
-
* Strip null, empty string, and empty array fields recursively.
|
|
36
|
-
* Preserves empty objects `{}` — these are F5 XC protobuf oneof presence markers
|
|
37
|
-
* (e.g. `use_origin_server_name: {}` means that option is selected).
|
|
38
|
-
*/
|
|
39
|
-
function stripEmpty(obj: unknown): unknown {
|
|
40
|
-
if (Array.isArray(obj)) return obj.map(stripEmpty).filter(v => v != null);
|
|
41
|
-
if (obj && typeof obj === "object") {
|
|
42
|
-
const entries = Object.entries(obj as Record<string, unknown>);
|
|
43
|
-
// Preserve source-empty objects (F5 XC oneof presence markers)
|
|
44
|
-
if (entries.length === 0) return obj;
|
|
45
|
-
const out: Record<string, unknown> = {};
|
|
46
|
-
for (const [k, v] of entries) {
|
|
47
|
-
if (v == null || v === "" || (Array.isArray(v) && v.length === 0)) continue;
|
|
48
|
-
const cleaned = stripEmpty(v);
|
|
49
|
-
if (cleaned != null) out[k] = cleaned;
|
|
50
|
-
}
|
|
51
|
-
return Object.keys(out).length > 0 ? out : null;
|
|
52
|
-
}
|
|
53
|
-
return obj;
|
|
54
|
-
}
|
|
55
|
-
function formatTimestamp(iso: string): string {
|
|
56
|
-
return iso.replace("T", " ").replace(/:\d{2}(\.\d+)?Z$/, " UTC");
|
|
57
|
-
}
|
|
58
40
|
function stripProtobufPrefix(message: string): string {
|
|
59
41
|
return message.replace(/^ves\.io\.schema\.\S+:\s*/i, "");
|
|
60
42
|
}
|
|
@@ -247,14 +229,7 @@ export const xcshApiToolRenderer = {
|
|
|
247
229
|
const sections: Array<{ label?: string; lines: string[] }> = [];
|
|
248
230
|
|
|
249
231
|
const addSection = (label: string, lines: string[], maxLines?: number): void => {
|
|
250
|
-
|
|
251
|
-
if (maxLines && lines.length > maxLines) {
|
|
252
|
-
const truncated = lines.slice(0, maxLines);
|
|
253
|
-
truncated.push(uiTheme.fg("dim", `… ${lines.length - maxLines} more lines`));
|
|
254
|
-
sections.push({ label: titled, lines: truncated });
|
|
255
|
-
} else {
|
|
256
|
-
sections.push({ label: titled, lines });
|
|
257
|
-
}
|
|
232
|
+
pushSection(sections, label, lines, uiTheme, maxLines);
|
|
258
233
|
};
|
|
259
234
|
|
|
260
235
|
// Section: Request payload — show resolved body (actual JSON sent to API)
|