@f5-sales-demo/xcsh 19.94.1 → 19.95.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": "@f5-sales-demo/xcsh",
4
- "version": "19.94.1",
4
+ "version": "19.95.0",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Can Boluk",
@@ -31,9 +31,9 @@
31
31
  "xcsh": "src/cli.ts"
32
32
  },
33
33
  "scripts": {
34
- "build": "bun run generate-build-info && bun run generate-extension-capabilities && bun run generate-api-spec-index && bun run generate-branding-index && bun run generate-terraform-index && bun run generate-console-catalog && bun run generate-console-field-metadata && test -f src/internal-urls/api-spec-index.generated.ts && bun --cwd=../stats scripts/generate-client-bundle.ts --generate && bun --cwd=../office-pane scripts/generate-client-bundle.ts --generate && bun --cwd=../natives run embed:native && bun build --compile --define PI_COMPILED=true --external mupdf --root ../.. ./src/cli.ts --outfile dist/xcsh && bun --cwd=../natives run embed:native --reset && bun --cwd=../stats scripts/generate-client-bundle.ts --reset && bun --cwd=../office-pane scripts/generate-client-bundle.ts --reset",
34
+ "build": "bun run generate-build-info && bun run generate-extension-capabilities && bun run generate-api-spec-index && bun run generate-branding-index && bun run generate-sitecli-index && bun run generate-terraform-index && bun run generate-console-catalog && bun run generate-console-field-metadata && test -f src/internal-urls/api-spec-index.generated.ts && bun --cwd=../stats scripts/generate-client-bundle.ts --generate && bun --cwd=../office-pane scripts/generate-client-bundle.ts --generate && bun --cwd=../natives run embed:native && bun build --compile --define PI_COMPILED=true --external mupdf --root ../.. ./src/cli.ts --outfile dist/xcsh && bun --cwd=../natives run embed:native --reset && bun --cwd=../stats scripts/generate-client-bundle.ts --reset && bun --cwd=../office-pane scripts/generate-client-bundle.ts --reset",
35
35
  "check": "biome check . && bun run format-prompts -- --check && bun run check:types",
36
- "check:types": "bun run generate-build-info && bun run generate-extension-capabilities && bun run generate-api-spec-index && bun run generate-branding-index && bun run generate-terraform-index && tsgo -p tsconfig.json --noEmit",
36
+ "check:types": "bun run generate-build-info && bun run generate-extension-capabilities && bun run generate-api-spec-index && bun run generate-branding-index && bun run generate-sitecli-index && bun run generate-terraform-index && tsgo -p tsconfig.json --noEmit",
37
37
  "lint": "biome lint .",
38
38
  "check:bundle": "bun scripts/check-bundle.ts",
39
39
  "test": "bun run generate-build-info && bun run generate-extension-capabilities && bun run generate-api-spec-index && bun run generate-console-catalog && bun run generate-console-field-metadata && bun scripts/bun-test-guarded.ts --max-concurrency 4",
@@ -44,6 +44,7 @@
44
44
  "generate-docs-index": "bun scripts/generate-docs-index.ts",
45
45
  "generate-api-spec-index": "bun scripts/generate-api-spec-index.ts",
46
46
  "generate-branding-index": "bun scripts/generate-branding-index.ts",
47
+ "generate-sitecli-index": "bun scripts/generate-sitecli-index.ts",
47
48
  "generate-build-info": "bun scripts/generate-build-info.ts",
48
49
  "generate-extension-capabilities": "bun scripts/generate-extension-capabilities.ts",
49
50
  "generate-terraform-index": "bun scripts/generate-terraform-index.ts",
@@ -56,13 +57,13 @@
56
57
  "dependencies": {
57
58
  "@agentclientprotocol/sdk": "1.3.0",
58
59
  "@mozilla/readability": "^0.6",
59
- "@f5-sales-demo/xcsh-stats": "19.94.1",
60
- "@f5-sales-demo/pi-agent-core": "19.94.1",
61
- "@f5-sales-demo/pi-ai": "19.94.1",
62
- "@f5-sales-demo/pi-natives": "19.94.1",
63
- "@f5-sales-demo/pi-resource-management": "19.94.1",
64
- "@f5-sales-demo/pi-tui": "19.94.1",
65
- "@f5-sales-demo/pi-utils": "19.94.1",
60
+ "@f5-sales-demo/xcsh-stats": "19.95.0",
61
+ "@f5-sales-demo/pi-agent-core": "19.95.0",
62
+ "@f5-sales-demo/pi-ai": "19.95.0",
63
+ "@f5-sales-demo/pi-natives": "19.95.0",
64
+ "@f5-sales-demo/pi-resource-management": "19.95.0",
65
+ "@f5-sales-demo/pi-tui": "19.95.0",
66
+ "@f5-sales-demo/pi-utils": "19.95.0",
66
67
  "@sinclair/typebox": "^0.34",
67
68
  "@xterm/headless": "^6.0",
68
69
  "ajv": "^8.20",
@@ -0,0 +1,104 @@
1
+ // Embeds the Customer Edge Site CLI command surface, so the agent knows it without a
2
+ // network round trip.
3
+ //
4
+ // The source of truth is sitecli/catalog.json in f5-sales-demo/mcn, which is produced
5
+ // by capturing the node's own self-describing catalog rather than transcribed from any
6
+ // document. Resolution order matches generate-terraform-index.ts and
7
+ // generate-branding-index.ts: a sibling checkout first, then raw.githubusercontent.
8
+ import * as fs from "node:fs/promises";
9
+ import * as path from "node:path";
10
+
11
+ const OUTPUT_FILE = path.join(import.meta.dir, "..", "src", "internal-urls", "sitecli-index.generated.ts");
12
+
13
+ const LOCAL_CATALOG_PATH = path.resolve(import.meta.dir, "..", "..", "..", "..", "mcn", "sitecli", "catalog.json");
14
+
15
+ const GITHUB_RAW_URL = "https://raw.githubusercontent.com/f5-sales-demo/mcn/main/sitecli/catalog.json";
16
+
17
+ /** Shape of sitecli/catalog.json as committed by mcn. */
18
+ interface SiteCliCatalog {
19
+ build: string;
20
+ source?: { site?: string; node?: string };
21
+ commands: Record<
22
+ string,
23
+ {
24
+ category: string;
25
+ /** "ExecUser" (read-only) or "Exec" (privileged, mutates or reads a state marker). */
26
+ tier: string;
27
+ /** Illustrative only — often a placeholder such as " container-id". */
28
+ example?: string;
29
+ /** "GLOBAL" means a dedicated GET endpoint, NOT reachable via exec-user. */
30
+ scope?: string;
31
+ }
32
+ >;
33
+ }
34
+
35
+ async function loadCatalog(): Promise<SiteCliCatalog> {
36
+ const localFile = Bun.file(LOCAL_CATALOG_PATH);
37
+ if (await localFile.exists()) {
38
+ console.log(`Reading from local checkout: ${LOCAL_CATALOG_PATH}`);
39
+ return localFile.json();
40
+ }
41
+
42
+ console.log(`Local not found, fetching from ${GITHUB_RAW_URL}`);
43
+ const headers: Record<string, string> = {};
44
+ const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
45
+ if (token) {
46
+ headers.Authorization = `token ${token}`;
47
+ }
48
+
49
+ const response = await fetch(GITHUB_RAW_URL, { headers });
50
+ if (!response.ok) {
51
+ throw new Error(`Failed to fetch sitecli catalog.json: ${response.status} ${response.statusText}`);
52
+ }
53
+ return (await response.json()) as SiteCliCatalog;
54
+ }
55
+
56
+ /**
57
+ * Which endpoint a command must be sent to. This is derived here rather than left to
58
+ * the caller because getting it wrong returns "command not supported" — the same
59
+ * message as a command that does not exist — so the mistake reads as a missing
60
+ * command rather than a wrong transport.
61
+ */
62
+ function transportFor(entry: { tier: string; scope?: string }): "global-get" | "exec-user" | "exec" {
63
+ if (entry.scope === "GLOBAL") return "global-get";
64
+ return entry.tier === "Exec" ? "exec" : "exec-user";
65
+ }
66
+
67
+ function generateTypeScript(catalog: SiteCliCatalog): string {
68
+ const commands = Object.fromEntries(
69
+ Object.entries(catalog.commands)
70
+ .sort(([a], [b]) => a.localeCompare(b))
71
+ .map(([name, entry]) => [
72
+ name,
73
+ {
74
+ category: entry.category,
75
+ tier: entry.tier,
76
+ transport: transportFor(entry),
77
+ mutating: entry.tier === "Exec",
78
+ ...(entry.example ? { example: entry.example.trim() } : {}),
79
+ ...(entry.scope ? { scope: entry.scope } : {}),
80
+ },
81
+ ]),
82
+ );
83
+
84
+ return `${[
85
+ "// AUTO-GENERATED — do not edit. Run `bun generate-sitecli-index` to regenerate.",
86
+ "//",
87
+ "// Source: f5-sales-demo/mcn sitecli/catalog.json, captured from a live Customer Edge.",
88
+ "// The command surface depends on the node software build, so SITECLI_BUILD records",
89
+ "// which build this describes.",
90
+ "",
91
+ `export const SITECLI_BUILD = ${JSON.stringify(catalog.build)};`,
92
+ "",
93
+ `export const SITECLI_SOURCE = ${JSON.stringify(catalog.source ?? {}, null, 2)} as const;`,
94
+ "",
95
+ `export const SITECLI_COMMANDS = ${JSON.stringify(commands, null, 2)} as const;`,
96
+ "",
97
+ ].join("\n")}`;
98
+ }
99
+
100
+ const catalog = await loadCatalog();
101
+ const output = generateTypeScript(catalog);
102
+ await fs.writeFile(OUTPUT_FILE, output, "utf-8");
103
+ await Bun.$`bunx biome format --write ${OUTPUT_FILE}`.quiet();
104
+ console.log(`Generated ${OUTPUT_FILE} (${Object.keys(catalog.commands).length} commands, build ${catalog.build})`);
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "19.94.1",
21
- "commit": "960cef12ede26aa043de205b6faf4a8becc0ea9c",
22
- "shortCommit": "960cef1",
20
+ "version": "19.95.0",
21
+ "commit": "6239a5a701200eab5295f16c6b7698898480d4b0",
22
+ "shortCommit": "6239a5a",
23
23
  "branch": "main",
24
- "tag": "v19.94.1",
25
- "commitDate": "2026-07-26T10:03:06Z",
26
- "buildDate": "2026-07-26T10:26:20.376Z",
24
+ "tag": "v19.95.0",
25
+ "commitDate": "2026-07-26T17:07:54Z",
26
+ "buildDate": "2026-07-26T17:33:27.485Z",
27
27
  "dirty": true,
28
28
  "prNumber": "",
29
29
  "repoUrl": "https://github.com/f5-sales-demo/xcsh",
30
30
  "repoSlug": "f5-sales-demo/xcsh",
31
- "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/960cef12ede26aa043de205b6faf4a8becc0ea9c",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.94.1"
31
+ "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/6239a5a701200eab5295f16c6b7698898480d4b0",
32
+ "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.95.0"
33
33
  };
@@ -0,0 +1,237 @@
1
+ // AUTO-GENERATED — do not edit. Run `bun generate-sitecli-index` to regenerate.
2
+ //
3
+ // Source: f5-sales-demo/mcn sitecli/catalog.json, captured from a live Customer Edge.
4
+ // The command surface depends on the node software build, so SITECLI_BUILD records
5
+ // which build this describes.
6
+
7
+ export const SITECLI_BUILD = "crt-20250613-3382";
8
+
9
+ export const SITECLI_SOURCE = {
10
+ node: "f5-xc-ce-vm-01",
11
+ site: "ar-bgp-eastus01",
12
+ } as const;
13
+
14
+ export const SITECLI_COMMANDS = {
15
+ "chronyc-sources": {
16
+ category: "Network Troubleshooting",
17
+ tier: "ExecUser",
18
+ transport: "exec-user",
19
+ mutating: false,
20
+ },
21
+ "crictl-images": {
22
+ category: "System Troubleshooting",
23
+ tier: "ExecUser",
24
+ transport: "exec-user",
25
+ mutating: false,
26
+ },
27
+ "crictl-inspect": {
28
+ category: "System Troubleshooting",
29
+ tier: "ExecUser",
30
+ transport: "exec-user",
31
+ mutating: false,
32
+ example: "container-id",
33
+ },
34
+ "crictl-logs": {
35
+ category: "System Troubleshooting",
36
+ tier: "ExecUser",
37
+ transport: "exec-user",
38
+ mutating: false,
39
+ example: "container-id",
40
+ },
41
+ "crictl-ps": {
42
+ category: "System Troubleshooting",
43
+ tier: "ExecUser",
44
+ transport: "exec-user",
45
+ mutating: false,
46
+ },
47
+ "crictl-ps-a": {
48
+ category: "System Troubleshooting",
49
+ tier: "ExecUser",
50
+ transport: "exec-user",
51
+ mutating: false,
52
+ },
53
+ "curl-host": {
54
+ category: "Network Troubleshooting",
55
+ tier: "ExecUser",
56
+ transport: "exec-user",
57
+ mutating: false,
58
+ example: "-v cloud.f5.com",
59
+ },
60
+ "curl-vega": {
61
+ category: "Network Troubleshooting",
62
+ tier: "ExecUser",
63
+ transport: "exec-user",
64
+ mutating: false,
65
+ example: "-v cloud.f5.com",
66
+ },
67
+ diagnosis: {
68
+ category: "System Troubleshooting",
69
+ tier: "ExecUser",
70
+ transport: "global-get",
71
+ mutating: false,
72
+ example: "no argument needed",
73
+ scope: "GLOBAL",
74
+ },
75
+ dig: {
76
+ category: "Network Troubleshooting",
77
+ tier: "ExecUser",
78
+ transport: "exec-user",
79
+ mutating: false,
80
+ example: "@192.168.0.2 http://volterra.azurecr.io",
81
+ },
82
+ "docker-images": {
83
+ category: "System Troubleshooting",
84
+ tier: "ExecUser",
85
+ transport: "exec-user",
86
+ mutating: false,
87
+ },
88
+ "docker-inspect": {
89
+ category: "System Troubleshooting",
90
+ tier: "ExecUser",
91
+ transport: "exec-user",
92
+ mutating: false,
93
+ example: "container-id OR name",
94
+ },
95
+ "docker-logs": {
96
+ category: "System Troubleshooting",
97
+ tier: "ExecUser",
98
+ transport: "exec-user",
99
+ mutating: false,
100
+ example: "container-id OR name",
101
+ },
102
+ "docker-ps": {
103
+ category: "System Troubleshooting",
104
+ tier: "ExecUser",
105
+ transport: "exec-user",
106
+ mutating: false,
107
+ },
108
+ "docker-ps-a": {
109
+ category: "System Troubleshooting",
110
+ tier: "ExecUser",
111
+ transport: "exec-user",
112
+ mutating: false,
113
+ },
114
+ dropstats: {
115
+ category: "Network Troubleshooting",
116
+ tier: "ExecUser",
117
+ transport: "exec-user",
118
+ mutating: false,
119
+ },
120
+ "dropstats-non-zero": {
121
+ category: "Network Troubleshooting",
122
+ tier: "ExecUser",
123
+ transport: "exec-user",
124
+ mutating: false,
125
+ },
126
+ "flow-l": {
127
+ category: "Network Troubleshooting",
128
+ tier: "ExecUser",
129
+ transport: "exec-user",
130
+ mutating: false,
131
+ },
132
+ "flow-l-match": {
133
+ category: "Network Troubleshooting",
134
+ tier: "ExecUser",
135
+ transport: "exec-user",
136
+ mutating: false,
137
+ example: "100.127.192.10:53",
138
+ },
139
+ health: {
140
+ category: "System Troubleshooting",
141
+ tier: "ExecUser",
142
+ transport: "global-get",
143
+ mutating: false,
144
+ example: "no argument needed",
145
+ scope: "GLOBAL",
146
+ },
147
+ ip: {
148
+ category: "Network Troubleshooting",
149
+ tier: "ExecUser",
150
+ transport: "exec-user",
151
+ mutating: false,
152
+ example: "help OR addr",
153
+ },
154
+ "ip-link-set": {
155
+ category: "Network Troubleshooting",
156
+ tier: "Exec",
157
+ transport: "exec",
158
+ mutating: true,
159
+ example: "(<device>||<group>) (up||down)",
160
+ },
161
+ "ip-link-show": {
162
+ category: "Network Troubleshooting",
163
+ tier: "ExecUser",
164
+ transport: "exec-user",
165
+ mutating: false,
166
+ },
167
+ "ipsec-status": {
168
+ category: "Network Troubleshooting",
169
+ tier: "ExecUser",
170
+ transport: "exec-user",
171
+ mutating: false,
172
+ },
173
+ "ipsec-statusall": {
174
+ category: "Network Troubleshooting",
175
+ tier: "ExecUser",
176
+ transport: "exec-user",
177
+ mutating: false,
178
+ },
179
+ journalctl: {
180
+ category: "System Troubleshooting",
181
+ tier: "ExecUser",
182
+ transport: "exec-user",
183
+ mutating: false,
184
+ example: "-u vpm -n 200",
185
+ },
186
+ netstat: {
187
+ category: "Network Troubleshooting",
188
+ tier: "ExecUser",
189
+ transport: "exec-user",
190
+ mutating: false,
191
+ },
192
+ nh: {
193
+ category: "Network Troubleshooting",
194
+ tier: "ExecUser",
195
+ transport: "exec-user",
196
+ mutating: false,
197
+ example: "--help OR --list",
198
+ },
199
+ rt: {
200
+ category: "Network Troubleshooting",
201
+ tier: "ExecUser",
202
+ transport: "exec-user",
203
+ mutating: false,
204
+ example: "--help OR --dump $vrf-id OR --get $ipv4 --vrf $vrf-id",
205
+ },
206
+ "show-ip-bgp": {
207
+ category: "Network Troubleshooting",
208
+ tier: "ExecUser",
209
+ transport: "exec-user",
210
+ mutating: false,
211
+ },
212
+ "show-ip-bgp-neighbors": {
213
+ category: "Network Troubleshooting",
214
+ tier: "ExecUser",
215
+ transport: "exec-user",
216
+ mutating: false,
217
+ },
218
+ "show-ip-bgp-neighbors-advertised-route": {
219
+ category: "Network Troubleshooting",
220
+ tier: "ExecUser",
221
+ transport: "exec-user",
222
+ mutating: false,
223
+ },
224
+ "show-ip-bgp-summary": {
225
+ category: "Network Troubleshooting",
226
+ tier: "ExecUser",
227
+ transport: "exec-user",
228
+ mutating: false,
229
+ },
230
+ vif: {
231
+ category: "Network Troubleshooting",
232
+ tier: "ExecUser",
233
+ transport: "exec-user",
234
+ mutating: false,
235
+ example: "--list",
236
+ },
237
+ } as const;
@@ -210,7 +210,7 @@ export const TERRAFORM_INDEX: TerraformIndex = {
210
210
  {
211
211
  name: "Monitoring",
212
212
  slug: "monitoring",
213
- description: "Log receivers, alert policies, APM, and global logging configuration",
213
+ description: "Log receivers, alert policies, alert templates, and global logging configuration",
214
214
  resource_count: 4,
215
215
  resources: ["alert_receiver", "alert_template", "global_log_receiver", "log_receiver"],
216
216
  },
@@ -68,6 +68,7 @@ const PLUGIN_HOST = "plugin";
68
68
  const CHANGES_HOST = "changes";
69
69
  const SOURCE_HOST = "source";
70
70
  const FLEET_HOST = "fleet";
71
+ const SITECLI_HOST = "sitecli";
71
72
  const EMPTY_INDEX: ApiSpecIndex = { version: "unavailable", timestamp: "", domains: [] };
72
73
  const EMPTY_CATALOG_INDEX: ApiCatalogIndex = {
73
74
  version: "unavailable",
@@ -202,6 +203,39 @@ interface BrandingDeprecationEntry {
202
203
  required_providers_block?: string;
203
204
  }
204
205
 
206
+ interface SiteCliCommand {
207
+ category: string;
208
+ tier: string;
209
+ transport: string;
210
+ mutating: boolean;
211
+ example?: string;
212
+ scope?: string;
213
+ }
214
+
215
+ let _siteCliCache: { build: string; commands: Record<string, SiteCliCommand> } | null = null;
216
+
217
+ /** Mirrors loadBranding(): lazily required, cached, and degrading to an empty index
218
+ * with a warning rather than throwing when the generated file is absent. */
219
+ function loadSiteCli(): { build: string; commands: Record<string, SiteCliCommand> } {
220
+ if (_siteCliCache) return _siteCliCache;
221
+ try {
222
+ const mod = require("./sitecli-index.generated") as {
223
+ SITECLI_BUILD?: string;
224
+ SITECLI_COMMANDS?: Record<string, SiteCliCommand>;
225
+ };
226
+ _siteCliCache = {
227
+ build: mod.SITECLI_BUILD ?? "unknown",
228
+ commands: (mod.SITECLI_COMMANDS ?? {}) as Record<string, SiteCliCommand>,
229
+ };
230
+ } catch (err) {
231
+ logger.warn("sitecli index unavailable, sitecli protocol disabled", {
232
+ error: err instanceof Error ? err.message : String(err),
233
+ });
234
+ _siteCliCache = { build: "unavailable", commands: {} };
235
+ }
236
+ return _siteCliCache;
237
+ }
238
+
205
239
  let _brandingCache: {
206
240
  version: string;
207
241
  canonical: Record<string, BrandingCanonicalEntry>;
@@ -412,6 +446,10 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
412
446
  return this.#resolveBranding(url);
413
447
  }
414
448
 
449
+ if (host === SITECLI_HOST) {
450
+ return this.#resolveSiteCli(url);
451
+ }
452
+
415
453
  if (host === USER_ROUTE) {
416
454
  return this.#resolveUserProfile(url);
417
455
  }
@@ -574,6 +612,62 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
574
612
  };
575
613
  }
576
614
 
615
+ /** Customer Edge Site CLI command surface. The transport field is the reason this
616
+ * is embedded rather than looked up: sending a command to the wrong endpoint
617
+ * returns "command not supported", which is indistinguishable from the command not
618
+ * existing, so guessing produces a confidently wrong answer. */
619
+ #resolveSiteCli(url: InternalUrl): InternalResource {
620
+ const subpath = (url.rawPathname ?? url.pathname).replace(/^\/+/, "").replace(/\/+$/, "");
621
+ const { build, commands } = loadSiteCli();
622
+
623
+ let content: string;
624
+ if (!subpath) {
625
+ const lines = [
626
+ `# Customer Edge Site CLI (build ${build})`,
627
+ "",
628
+ "Transport is decided by the entry, not by preference. Sending a command to the",
629
+ "wrong endpoint returns `command not supported` — the same message as a command",
630
+ "that does not exist.",
631
+ "",
632
+ "| Command | Category | Tier | Transport |",
633
+ "| --- | --- | --- | --- |",
634
+ ];
635
+ for (const [name, c] of Object.entries(commands)) {
636
+ lines.push(`| ${name} | ${c.category} | ${c.tier} | ${c.transport}${c.mutating ? " (MUTATES)" : ""} |`);
637
+ }
638
+ lines.push("", `Read \`xcsh://sitecli/<command>\` for one entry.`);
639
+ content = lines.join("\n");
640
+ } else {
641
+ const entry = commands[subpath];
642
+ content = entry
643
+ ? [
644
+ `# ${subpath}`,
645
+ "",
646
+ `- category: ${entry.category}`,
647
+ `- tier: ${entry.tier}`,
648
+ `- transport: ${entry.transport}`,
649
+ `- mutating: ${entry.mutating}`,
650
+ ...(entry.scope ? [`- scope: ${entry.scope}`] : []),
651
+ ...(entry.example ? [`- example argument: \`${entry.example}\``] : []),
652
+ "",
653
+ entry.transport === "global-get"
654
+ ? "GLOBAL scope: GET .../vpm/debug/global/" + subpath + " — NOT reachable via exec-user."
655
+ : entry.transport === "exec"
656
+ ? "Exec tier: privileged and mutating. Do not run speculatively."
657
+ : 'POST .../vpm/debug/{node}/exec-user with {"command":["' + subpath + '", ...]}.',
658
+ ].join("\n")
659
+ : `Unknown Site CLI command: ${subpath}\n\nRead xcsh://sitecli for the full list (build ${build}).`;
660
+ }
661
+
662
+ return {
663
+ url: url.href,
664
+ content,
665
+ contentType: "text/markdown",
666
+ size: Buffer.byteLength(content, "utf-8"),
667
+ sourcePath: `xcsh://sitecli${subpath ? `/${subpath}` : ""}`,
668
+ };
669
+ }
670
+
577
671
  #brandingOverview(): string {
578
672
  const { version, canonical, deprecations } = loadBranding();
579
673
  const lines = [`# F5 Distributed Cloud Branding (v${version})`, "", "## Product Names (Current API)", ""];
@@ -0,0 +1,83 @@
1
+ Run diagnostic commands on an F5 Distributed Cloud Customer Edge node.
2
+
3
+ The Site CLI runs on the CE itself and is reached over the `vpm/debug` API. The command
4
+ surface is embedded at `xcsh://sitecli` — read it rather than guessing, because command
5
+ availability depends on the node software build.
6
+
7
+ <critical>
8
+ The transport is decided by the command's catalog entry, NOT by preference. Sending a
9
+ command to the wrong endpoint returns `command not supported`, which is the SAME message
10
+ returned for a command that does not exist. Reporting "that command is unavailable" when
11
+ the transport was wrong is a confidently wrong answer, and it is the most common failure
12
+ here.
13
+ </critical>
14
+
15
+ ## Three transports
16
+
17
+ |Entry says|Send to|Returns|
18
+ |---|---|---|
19
+ |`transport: global-get`|`GET …/vpm/debug/global/<cmd>`|JSON|
20
+ |`transport: exec-user`|`POST …/vpm/debug/<node>/exec-user`|text|
21
+ |`transport: exec`|`POST …/vpm/debug/<node>/exec`|text|
22
+
23
+ `exec-user` and `exec` take a body of the form:
24
+
25
+ ```json
26
+ { "namespace": "system", "site": "<site>", "node": "<node>", "command": ["<cmd>", "<arg>", "..."] }
27
+ ```
28
+
29
+ Arguments are separate array elements, not one string:
30
+ `["journalctl", "-u", "vpm", "-n", "200"]`.
31
+
32
+ `global-get` takes no body and no arguments. Only `health` and `diagnosis` use it.
33
+
34
+ <prohibited>
35
+ Never run a command whose entry has `mutating: true` unless the user has asked for that
36
+ specific change and understands the node is live. On the current build that is
37
+ `ip-link-set`, which takes an interface down — including, on a remote CE, the interface
38
+ you are managing it through. Newer builds add `systemctl-restart-NetworkManager`,
39
+ `systemctl-restart-crio`, `systemctl-restart-kubelet` and `systemctl-start-crio-prune`,
40
+ which restart services under a live data plane.
41
+ </prohibited>
42
+
43
+ ## Discovering the surface on an unknown node
44
+
45
+ POST to `exec-user` with the `command` key **omitted**. That returns the node's own
46
+ catalog. Sending an unknown command value instead returns an error, not the catalog.
47
+
48
+ ## Before running anything, check the node is reachable
49
+
50
+ The `vpm/debug` API is served through the F5 Distributed Cloud control plane over the
51
+ tunnel that registration establishes. It answers only when the site is `ONLINE`:
52
+
53
+ ```
54
+ GET /api/config/namespaces/system/sites/<site> -> .spec.site_state
55
+ ```
56
+
57
+ If the site is not `ONLINE`, no command on this page will work, and the node also
58
+ refuses SSH as shipped. The only remaining route is the Azure Serial Console, which
59
+ needs a human at a terminal.
60
+
61
+ ## Reading results
62
+ - `health` first. It is cheap and structured, and `state: PROVISIONED` confirms the node
63
+ registered and is configured.
64
+ - Long output is genuinely long: `nh --list` runs to thousands of lines and the flow
65
+ table holds hundreds of thousands of entries. Prefer `flow-l-match <ip>[:port]` over
66
+ `flow-l`, and `dropstats-non-zero` over `dropstats`.
67
+ - Drop counters are lifetime totals, not rates. A non-zero counter means nothing on its
68
+ own; run the command twice and compare while reproducing the problem.
69
+
70
+ <instruction>
71
+ Two traps that produce plausible but wrong conclusions:
72
+ 1. `rt --dump` takes the vRouter table id, which is NOT the `vrf-id` that
73
+ `show-ip-bgp-summary` prints. Passing the BGP number returns
74
+ `No such file or directory`, which reads as a broken node rather than a wrong
75
+ argument.
76
+ 2. The node runs Docker AND CRI-O simultaneously. `vpm`, `argo_watch` and
77
+ `site-console` are Docker containers; Kubernetes workloads are CRI-O. A three-entry
78
+ `docker-ps` is the complete list, not a symptom, and `crictl-*` will not show `vpm`.
79
+ </instruction>
80
+
81
+ For anything beyond the command surface — what output means, and the diagnostic
82
+ sequences that chain commands — read the Customer Edge documentation in the `mcn`
83
+ documentation set rather than inferring it.