@f5xc-salesdemos/xcsh 19.8.0 → 19.9.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/examples/extensions/todo.ts +1 -1
- package/package.json +7 -7
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/lsp/render.ts +1 -1
- package/src/mcp/oauth-flow.ts +2 -2
- package/src/modes/components/agent-dashboard.ts +1 -1
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/plugins/plugin-dashboard.ts +37 -1
- package/src/modes/components/plugins/plugin-list-pane.ts +7 -3
- package/src/prompts/tools/xcsh-api.md +31 -0
- package/src/session/agent-session.ts +2 -2
- package/src/vim/engine.ts +3 -3
|
@@ -118,7 +118,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
118
118
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
119
119
|
if (entry.type !== "message") continue;
|
|
120
120
|
const msg = (entry as { message?: { role?: string; toolName?: string; details?: unknown } }).message;
|
|
121
|
-
if (
|
|
121
|
+
if (msg?.role !== "toolResult" || msg.toolName !== "todo") continue;
|
|
122
122
|
|
|
123
123
|
const details = msg.details as TodoDetails | undefined;
|
|
124
124
|
if (details) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "19.
|
|
4
|
+
"version": "19.9.0",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5xc-salesdemos/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -50,12 +50,12 @@
|
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
52
52
|
"@mozilla/readability": "^0.6",
|
|
53
|
-
"@f5xc-salesdemos/xcsh-stats": "19.
|
|
54
|
-
"@f5xc-salesdemos/pi-agent-core": "19.
|
|
55
|
-
"@f5xc-salesdemos/pi-ai": "19.
|
|
56
|
-
"@f5xc-salesdemos/pi-natives": "19.
|
|
57
|
-
"@f5xc-salesdemos/pi-tui": "19.
|
|
58
|
-
"@f5xc-salesdemos/pi-utils": "19.
|
|
53
|
+
"@f5xc-salesdemos/xcsh-stats": "19.9.0",
|
|
54
|
+
"@f5xc-salesdemos/pi-agent-core": "19.9.0",
|
|
55
|
+
"@f5xc-salesdemos/pi-ai": "19.9.0",
|
|
56
|
+
"@f5xc-salesdemos/pi-natives": "19.9.0",
|
|
57
|
+
"@f5xc-salesdemos/pi-tui": "19.9.0",
|
|
58
|
+
"@f5xc-salesdemos/pi-utils": "19.9.0",
|
|
59
59
|
"@sinclair/typebox": "^0.34",
|
|
60
60
|
"@xterm/headless": "^6.0",
|
|
61
61
|
"ajv": "^8.20",
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "19.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "19.9.0",
|
|
21
|
+
"commit": "146551aadd2d3231eb436e0f5f760c96dc3ee602",
|
|
22
|
+
"shortCommit": "146551a",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.
|
|
25
|
-
"commitDate": "2026-06-
|
|
26
|
-
"buildDate": "2026-06-
|
|
24
|
+
"tag": "v19.9.0",
|
|
25
|
+
"commitDate": "2026-06-05T02:16:52Z",
|
|
26
|
+
"buildDate": "2026-06-05T02:38:22.277Z",
|
|
27
27
|
"dirty": true,
|
|
28
28
|
"prNumber": "",
|
|
29
29
|
"repoUrl": "https://github.com/f5xc-salesdemos/xcsh",
|
|
30
30
|
"repoSlug": "f5xc-salesdemos/xcsh",
|
|
31
|
-
"commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/
|
|
32
|
-
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.
|
|
31
|
+
"commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/146551aadd2d3231eb436e0f5f760c96dc3ee602",
|
|
32
|
+
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.9.0"
|
|
33
33
|
};
|
package/src/lsp/render.ts
CHANGED
|
@@ -110,7 +110,7 @@ export function renderResult(
|
|
|
110
110
|
args?: LspParams,
|
|
111
111
|
): Component {
|
|
112
112
|
const content = result.content?.[0];
|
|
113
|
-
if (
|
|
113
|
+
if (content?.type !== "text" || !("text" in content) || !content.text) {
|
|
114
114
|
const icon = formatStatusIcon("warning", theme, options.spinnerFrame);
|
|
115
115
|
const header = `${icon} LSP`;
|
|
116
116
|
return new Text([header, theme.fg("dim", "No result")].join("\n"), 0, 0);
|
package/src/mcp/oauth-flow.ts
CHANGED
|
@@ -42,7 +42,7 @@ function getUriPort(uri: URL): number {
|
|
|
42
42
|
|
|
43
43
|
function validateRedirectConfig(config: MCPOAuthConfig, redirectUri: string | undefined): void {
|
|
44
44
|
const parsed = parseRedirectUri(redirectUri);
|
|
45
|
-
if (
|
|
45
|
+
if (parsed?.protocol !== "https:" || !isLoopbackHostname(parsed.hostname)) {
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -63,7 +63,7 @@ function resolveCallbackPort(callbackPort: number | undefined, redirectUri: stri
|
|
|
63
63
|
if (callbackPort !== undefined) return callbackPort;
|
|
64
64
|
|
|
65
65
|
const parsed = parseRedirectUri(redirectUri);
|
|
66
|
-
if (
|
|
66
|
+
if (parsed?.protocol !== "http:" || !isLoopbackHostname(parsed.hostname)) {
|
|
67
67
|
return DEFAULT_PORT;
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -121,7 +121,7 @@ function matchAgent(agent: DashboardAgent, query: string): boolean {
|
|
|
121
121
|
function extractAssistantText(messages: AgentMessage[]): string | null {
|
|
122
122
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
123
123
|
const message = messages[i];
|
|
124
|
-
if (
|
|
124
|
+
if (message?.role !== "assistant") continue;
|
|
125
125
|
const blocks = message.content;
|
|
126
126
|
if (!Array.isArray(blocks)) continue;
|
|
127
127
|
const text = blocks
|
|
@@ -132,7 +132,7 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
|
|
|
132
132
|
const removedLines: { lineNum: string; content: string }[] = [];
|
|
133
133
|
while (i < lines.length) {
|
|
134
134
|
const p = parseDiffLine(lines[i]);
|
|
135
|
-
if (
|
|
135
|
+
if (p?.prefix !== "-") break;
|
|
136
136
|
removedLines.push({ lineNum: p.lineNum, content: p.content });
|
|
137
137
|
i++;
|
|
138
138
|
}
|
|
@@ -141,7 +141,7 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
|
|
|
141
141
|
const addedLines: { lineNum: string; content: string }[] = [];
|
|
142
142
|
while (i < lines.length) {
|
|
143
143
|
const p = parseDiffLine(lines[i]);
|
|
144
|
-
if (
|
|
144
|
+
if (p?.prefix !== "+") break;
|
|
145
145
|
addedLines.push({ lineNum: p.lineNum, content: p.content });
|
|
146
146
|
i++;
|
|
147
147
|
}
|
|
@@ -251,7 +251,7 @@ export class PluginDashboard extends Container {
|
|
|
251
251
|
if (!plugin) return;
|
|
252
252
|
const tabId = this.#activeTabId();
|
|
253
253
|
|
|
254
|
-
if (tabId === "discover" && !plugin.installed) {
|
|
254
|
+
if ((tabId === "discover" || tabId === "recommended") && !plugin.installed) {
|
|
255
255
|
await this.#installPlugin(plugin);
|
|
256
256
|
} else if (tabId === "updates" && plugin.hasUpdate) {
|
|
257
257
|
await this.#upgradePlugin(plugin);
|
|
@@ -289,6 +289,35 @@ export class PluginDashboard extends Container {
|
|
|
289
289
|
}
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
async #installAllRecommended(): Promise<void> {
|
|
293
|
+
const recommended = this.#state.allPlugins.filter(p => !p.installed && p.recommended && p.marketplace);
|
|
294
|
+
if (recommended.length === 0) {
|
|
295
|
+
this.#state.notice = "All recommended plugins are already installed";
|
|
296
|
+
this.#rebuildAndRender();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.#state.notice = `Installing ${recommended.length} recommended plugin(s)...`;
|
|
301
|
+
this.#rebuildAndRender();
|
|
302
|
+
|
|
303
|
+
let installed = 0;
|
|
304
|
+
let failed = 0;
|
|
305
|
+
for (const plugin of recommended) {
|
|
306
|
+
try {
|
|
307
|
+
await this.#mgr.installPlugin(plugin.name, plugin.marketplace!);
|
|
308
|
+
installed++;
|
|
309
|
+
} catch {
|
|
310
|
+
failed++;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
this.#state.notice =
|
|
315
|
+
failed > 0
|
|
316
|
+
? `Installed ${installed}/${recommended.length} recommended plugin(s), ${failed} failed`
|
|
317
|
+
: `Installed ${installed} recommended plugin(s)`;
|
|
318
|
+
await this.#reloadData();
|
|
319
|
+
}
|
|
320
|
+
|
|
292
321
|
async #upgradePlugin(plugin: DashboardPlugin): Promise<void> {
|
|
293
322
|
this.#state.notice = `Upgrading ${plugin.name}...`;
|
|
294
323
|
this.#rebuildAndRender();
|
|
@@ -320,6 +349,8 @@ export class PluginDashboard extends Container {
|
|
|
320
349
|
#getHelpText(): string {
|
|
321
350
|
const tabId = this.#activeTabId();
|
|
322
351
|
switch (tabId) {
|
|
352
|
+
case "recommended":
|
|
353
|
+
return " ↑/↓: navigate Enter: install A: install all Tab: next tab Ctrl+R: reload Esc: close";
|
|
323
354
|
case "discover":
|
|
324
355
|
return " ↑/↓: navigate Enter: install Tab: next tab Ctrl+R: reload Esc: close";
|
|
325
356
|
case "updates":
|
|
@@ -421,6 +452,11 @@ export class PluginDashboard extends Container {
|
|
|
421
452
|
return;
|
|
422
453
|
}
|
|
423
454
|
|
|
455
|
+
if (data.toLowerCase() === "a" && this.#activeTabId() === "recommended") {
|
|
456
|
+
void this.#installAllRecommended();
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
424
460
|
if (data.toLowerCase() === "u") {
|
|
425
461
|
const plugin = this.#selectedPlugin();
|
|
426
462
|
if (plugin?.hasUpdate) {
|
|
@@ -24,9 +24,11 @@ export class PluginListPane implements Component {
|
|
|
24
24
|
const msg =
|
|
25
25
|
this.activeTab === "discover"
|
|
26
26
|
? "No plugins available. Add a marketplace first."
|
|
27
|
-
: this.activeTab === "
|
|
28
|
-
? "All plugins are
|
|
29
|
-
:
|
|
27
|
+
: this.activeTab === "recommended"
|
|
28
|
+
? "All recommended plugins are installed."
|
|
29
|
+
: this.activeTab === "updates"
|
|
30
|
+
? "All plugins are up to date."
|
|
31
|
+
: "No plugins installed.";
|
|
30
32
|
lines.push(theme.fg("muted", ` ${msg}`));
|
|
31
33
|
return lines;
|
|
32
34
|
}
|
|
@@ -66,6 +68,8 @@ export class PluginListPane implements Component {
|
|
|
66
68
|
} else {
|
|
67
69
|
parts.push(theme.fg("dim", theme.status.disabled));
|
|
68
70
|
}
|
|
71
|
+
} else if (plugin.recommended) {
|
|
72
|
+
parts.push(theme.fg("warning", "*"));
|
|
69
73
|
} else {
|
|
70
74
|
parts.push(theme.fg("dim", "·"));
|
|
71
75
|
}
|
|
@@ -22,3 +22,34 @@ API calls to the same F5 XC tenant reuse a single TLS connection — sequential
|
|
|
22
22
|
|
|
23
23
|
**Relationship queries**: When the batch response says "Inventory complete" and includes a `Resource relationships:` section, the specs and relationships are already fully fetched. Answer directly from that data. Do NOT make additional GET calls to read individual resources you already have from the batch.
|
|
24
24
|
**Tenant-wide queries**: When asked about resources across ALL namespaces (e.g. "show all LBs in the entire tenant"), use `paths: ["*"]` with `params: {namespace: "*"}` to batch every namespace in ONE call. Do NOT list namespaces first — the wildcard handles discovery automatically.
|
|
25
|
+
|
|
26
|
+
**HTTP load balancer with origin pool** — when asked to create an LB routing to a named pool, use this exact payload structure (POST to `http_loadbalancers`):
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"metadata": { "name": "<lb-name>", "namespace": "<ns>" },
|
|
31
|
+
"spec": {
|
|
32
|
+
"domains": ["<domain>"],
|
|
33
|
+
"advertise_on_public_default_vip": {},
|
|
34
|
+
"http": { "port": 80 },
|
|
35
|
+
"default_route_pools": [
|
|
36
|
+
{ "pool": { "namespace": "<ns>", "name": "<pool-name>" }, "weight": 1, "priority": 1 }
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
For HTTPS: replace `"http": {"port": 80}` with `"https_auto_cert": {"http_redirect": true, "default_header": {}, "tls_config": {"default_security": {}}, "no_mtls": {}}`. For no pool (advertise only): omit `default_route_pools`.
|
|
43
|
+
|
|
44
|
+
**Resource disambiguation**: Several F5 XC resource types have similar names but different API paths. When the user's intent maps to one of these, use the exact API path shown:
|
|
45
|
+
|
|
46
|
+
|User says|Catalog category|API path segment|NOT|
|
|
47
|
+
|---|---|---|---|
|
|
48
|
+
|"rate limiter policy"|`rate-limiter-policys`|`rate_limiter_policys`|`policers`, `rate_limiters`|
|
|
49
|
+
|"policer"|`policers`|`policers`|`rate_limiter_policys`|
|
|
50
|
+
|"rate limiter"|`rate-limiters`|`rate_limiters`|`rate_limiter_policys`, `policers`|
|
|
51
|
+
|
|
52
|
+
**payload schemas for rate-limiting resources:**
|
|
53
|
+
- `policers` / "policer": `{"metadata":{"name":"<n>","namespace":"<ns>"},"spec":{"burst_size":<int>,"committed_information_rate":<int>}}` — network-level byte/Mbps limiting
|
|
54
|
+
- `rate_limiters` / "rate limiter": `{"metadata":{"name":"<n>","namespace":"<ns>"},"spec":{"burst_size":<int>,"committed_information_rate":<int>}}` — HTTP request-level limiting (rps)
|
|
55
|
+
- `rate_limiter_policys` / "rate limiter policy": requires existing `rate_limiter` reference; use `{"metadata":{"name":"<n>","namespace":"<ns>"},"spec":{"any_server":{},"rules":[{"metadata":{"name":"r"},"spec":{"any_client":{},"any_ip":{},"rate_limiter":{"namespace":"<ns>","name":"<rl-name>"}}}]}}`
|
|
@@ -4517,7 +4517,7 @@ export class AgentSession {
|
|
|
4517
4517
|
|
|
4518
4518
|
#closeCodexProviderSessionsForHistoryRewrite(): void {
|
|
4519
4519
|
const currentModel = this.model;
|
|
4520
|
-
if (
|
|
4520
|
+
if (currentModel?.api !== "openai-codex-responses") return;
|
|
4521
4521
|
this.#closeProviderSessionsForModelSwitch(currentModel, currentModel);
|
|
4522
4522
|
}
|
|
4523
4523
|
|
|
@@ -6115,7 +6115,7 @@ export class AgentSession {
|
|
|
6115
6115
|
const previousSessionFile = this.sessionFile;
|
|
6116
6116
|
const selectedEntry = this.sessionManager.getEntry(entryId);
|
|
6117
6117
|
|
|
6118
|
-
if (
|
|
6118
|
+
if (selectedEntry?.type !== "message" || selectedEntry.message.role !== "user") {
|
|
6119
6119
|
throw new Error("Invalid entry ID for branching");
|
|
6120
6120
|
}
|
|
6121
6121
|
|
package/src/vim/engine.ts
CHANGED
|
@@ -867,7 +867,7 @@ export class VimEngine {
|
|
|
867
867
|
}
|
|
868
868
|
case "r": {
|
|
869
869
|
const replacement = tokens[nextIndex + 1];
|
|
870
|
-
if (
|
|
870
|
+
if (replacement?.value.length !== 1) {
|
|
871
871
|
throw new VimError("Visual replace requires a literal character", opToken);
|
|
872
872
|
}
|
|
873
873
|
const visual = expandVisualOffsets(
|
|
@@ -1134,7 +1134,7 @@ export class VimEngine {
|
|
|
1134
1134
|
return nextIndex + 1;
|
|
1135
1135
|
case "r": {
|
|
1136
1136
|
const replacement = tokens[nextIndex + 1];
|
|
1137
|
-
if (
|
|
1137
|
+
if (replacement?.value.length !== 1) {
|
|
1138
1138
|
throw new VimError("r requires a replacement character", token);
|
|
1139
1139
|
}
|
|
1140
1140
|
await this.#applyAtomicChange(["r", replacement.value], () => {
|
|
@@ -1763,7 +1763,7 @@ export class VimEngine {
|
|
|
1763
1763
|
case "t":
|
|
1764
1764
|
case "T": {
|
|
1765
1765
|
const searchToken = tokens[index + 1];
|
|
1766
|
-
if (
|
|
1766
|
+
if (searchToken?.value.length !== 1) {
|
|
1767
1767
|
throw new VimError(`${token.value} requires a literal character`, token);
|
|
1768
1768
|
}
|
|
1769
1769
|
this.lastCharFind = { char: searchToken.value, mode: token.value as "f" | "F" | "t" | "T" };
|